From beb18006dacb2ae2ba2cddbb078bd8ec6a0c0c12 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 19 Feb 2020 20:18:02 +0100 Subject: [PATCH 001/387] Show 0 pp if map is loved --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- .../Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index f1250679c1..7baf6c7fcb 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -154,7 +154,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { new OsuSpriteText { - Text = $@"{score.PP:N0}", + Text = $@"{score.PP ?? 0:N0}", Font = OsuFont.GetFont(size: text_size) }, new FillFlowContainer diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index a15dc57d23..0616871897 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores totalScoreColumn.Text = $@"{value.TotalScore:N0}"; accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; - ppColumn.Text = $@"{value.PP:N0}"; + ppColumn.Text = $@"{value.PP ?? 0:N0}"; statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); modsColumn.Mods = value.Mods; From 397e35d0a0128adf174a7539f13a2a70da153762 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 26 Feb 2020 21:36:52 +0100 Subject: [PATCH 002/387] Hide pp column if map is loved or qualified --- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 36 +++++++++---------- .../Scores/TopScoreStatisticsSection.cs | 3 +- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 7baf6c7fcb..c9008adc85 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -88,11 +88,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); - columns.AddRange(new[] - { - new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30)), - new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)), - }); + if (score.PP.HasValue) + columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); + + columns.Add(new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); return columns.ToArray(); } @@ -150,24 +149,25 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }); } - content.AddRange(new Drawable[] + if (score.PP.HasValue) { - new OsuSpriteText + content.Add(new OsuSpriteText { - Text = $@"{score.PP ?? 0:N0}", + Text = $@"{score.PP:N0}", Font = OsuFont.GetFont(size: text_size) - }, - new FillFlowContainer + }); + } + + content.Add(new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(1), + ChildrenEnumerable = score.Mods.Select(m => new ModIcon(m) { - Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Spacing = new Vector2(1), - ChildrenEnumerable = score.Mods.Select(m => new ModIcon(m) - { - AutoSizeAxes = Axes.Both, - Scale = new Vector2(0.3f) - }) - }, + Scale = new Vector2(0.3f) + }) }); return content.ToArray(); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 0616871897..12ad014f61 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -96,7 +96,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores totalScoreColumn.Text = $@"{value.TotalScore:N0}"; accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; - ppColumn.Text = $@"{value.PP ?? 0:N0}"; + ppColumn.Alpha = value.PP.HasValue ? 1 : 0; + ppColumn.Text = $@"{value.PP:N0}"; statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); modsColumn.Mods = value.Mods; From 036f155afe755d961c99fa891cc3b9095b733cf6 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 28 Feb 2020 21:09:31 +0100 Subject: [PATCH 003/387] Adjust colours in DrawableMostPlayedBeatmap --- .../Historical/DrawableMostPlayedBeatmap.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index e75ad2f161..dc8492562a 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -37,8 +37,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { + ProfileItemContainer container; + AddRangeInternal(new Drawable[] { new UpdateableBeatmapSetCover @@ -61,7 +63,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical CornerRadius = corner_radius, Children = new Drawable[] { - new ProfileItemContainer + container = new ProfileItemContainer { Child = new Container { @@ -78,11 +80,14 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Children = new Drawable[] { new MostPlayedBeatmapMetadataContainer(beatmap), - new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular)) + new LinkFlowContainer(t => + { + t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); + t.Colour = colourProvider.Foreground1; + }) { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Colour = colours.GreySeafoamLighter }.With(d => { d.AddText("mapped by "); @@ -103,6 +108,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } }); + + container.IdleColour = colourProvider.Background4; + container.HoverColour = colourProvider.Background3; } private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer From 5838af39c1a20cc8bc1dc5fc6aab93c4dc82403f Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 28 Feb 2020 21:09:49 +0100 Subject: [PATCH 004/387] Add background colour customization to ProfileItemContainer --- .../Profile/Sections/ProfileItemContainer.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs index f65c909155..a48f21f52b 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs @@ -16,12 +16,19 @@ namespace osu.Game.Overlays.Profile.Sections protected override Container Content => content; - private Color4 idleColour; - private Color4 hoverColour; - private readonly Box background; private readonly Container content; + private Color4 idleColour; + + public Color4 IdleColour + { + get => idleColour; + set => idleColour = background.Colour = value; + } + + public Color4 HoverColour { get; set; } + public ProfileItemContainer() { RelativeSizeAxes = Axes.Both; @@ -44,20 +51,20 @@ namespace osu.Game.Overlays.Profile.Sections [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - background.Colour = idleColour = colourProvider.Background3; - hoverColour = colourProvider.Background2; + IdleColour = colourProvider.Background3; + HoverColour = colourProvider.Background2; } protected override bool OnHover(HoverEvent e) { - background.FadeColour(hoverColour, hover_duration, Easing.OutQuint); + background.FadeColour(HoverColour, hover_duration, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - background.FadeColour(idleColour, hover_duration, Easing.OutQuint); + background.FadeColour(IdleColour, hover_duration, Easing.OutQuint); } } } From d71b51690235187b31fb73a28f70d1cedf583dd8 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 28 Feb 2020 21:58:37 +0100 Subject: [PATCH 005/387] Check beatmap ranking status instead of the pp value --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 10 +++++++--- .../BeatmapSet/Scores/TopScoreStatisticsSection.cs | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index c9008adc85..25537537d9 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -28,6 +29,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly FillFlowContainer backgroundFlow; private Color4 highAccuracyColour; + private bool isBeatmapRanked; public ScoreTable() { @@ -65,7 +67,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores for (int i = 0; i < value.Count; i++) backgroundFlow.Add(new ScoreTableRowBackground(i, value[i], row_height)); - Columns = createHeaders(value[0]); + isBeatmapRanked = value.First().Beatmap.Status == BeatmapSetOnlineStatus.Ranked; + + Columns = createHeaders(value.First()); Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); } } @@ -88,7 +92,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); - if (score.PP.HasValue) + if (isBeatmapRanked) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); columns.Add(new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); @@ -149,7 +153,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }); } - if (score.PP.HasValue) + if (isBeatmapRanked) { content.Add(new OsuSpriteText { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 12ad014f61..9ecc40eed2 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; @@ -96,7 +97,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores totalScoreColumn.Text = $@"{value.TotalScore:N0}"; accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; - ppColumn.Alpha = value.PP.HasValue ? 1 : 0; + ppColumn.Alpha = value.Beatmap.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; ppColumn.Text = $@"{value.PP:N0}"; statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); From f661806513767adaf69f5989bcf4b3b90397f994 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Sat, 29 Feb 2020 15:29:00 +0100 Subject: [PATCH 006/387] Move checking logic out of ScoreTable --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 9 ++++----- osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 25537537d9..3a58f481e1 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -29,7 +29,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly FillFlowContainer backgroundFlow; private Color4 highAccuracyColour; - private bool isBeatmapRanked; public ScoreTable() { @@ -67,13 +66,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores for (int i = 0; i < value.Count; i++) backgroundFlow.Add(new ScoreTableRowBackground(i, value[i], row_height)); - isBeatmapRanked = value.First().Beatmap.Status == BeatmapSetOnlineStatus.Ranked; - Columns = createHeaders(value.First()); Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); } } + public bool IsBeatmapRanked { get; set; } + private TableColumn[] createHeaders(ScoreInfo score) { var columns = new List @@ -92,7 +91,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); - if (isBeatmapRanked) + if (IsBeatmapRanked) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); columns.Add(new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); @@ -153,7 +152,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }); } - if (isBeatmapRanked) + if (IsBeatmapRanked) { content.Add(new OsuSpriteText { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index e831c8ce42..5a931fffcb 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -59,11 +59,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList(); + var topScore = scoreInfos.First(); scoreTable.Scores = scoreInfos; + scoreTable.IsBeatmapRanked = topScore.Beatmap.Status == BeatmapSetOnlineStatus.Ranked; scoreTable.Show(); - var topScore = scoreInfos.First(); var userScore = value.UserScore; var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets); From 4d19278df498a940b2356d84a00eeb77456cc6e7 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Sat, 29 Feb 2020 15:43:48 +0100 Subject: [PATCH 007/387] Remove using directive --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 3a58f481e1..af6bf8299f 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; From bca58ddb42f73cc79116b1450c4db446bd015577 Mon Sep 17 00:00:00 2001 From: naoey Date: Sat, 29 Feb 2020 21:07:42 +0530 Subject: [PATCH 008/387] Make KeyCounter stop counting during breaks --- .../Visual/Gameplay/TestSceneAutoplay.cs | 16 ++++++++++++---- osu.Game/Screens/Play/Player.cs | 10 ++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 4daab8d137..ebd89039a5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -24,10 +24,17 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); - AddStep("rewind", () => track.Seek(-10000)); - AddUntilStep("key counter reset", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + ScoreAccessiblePlayer scoreAccessiblePlayer = null; + + AddUntilStep("player loaded", () => (scoreAccessiblePlayer = (ScoreAccessiblePlayer)Player) != null); + AddUntilStep("score above zero", () => scoreAccessiblePlayer.ScoreProcessor.TotalScore.Value > 0); + AddUntilStep("key counter counted keys", () => scoreAccessiblePlayer.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); + AddStep("seek to break time", () => scoreAccessiblePlayer.GameplayClockContainer.Seek(scoreAccessiblePlayer.BreakOverlay.Breaks.First().StartTime)); + AddUntilStep("wait for seek to complete", () => + scoreAccessiblePlayer.HUDOverlay.Progress.ReferenceClock.CurrentTime >= scoreAccessiblePlayer.BreakOverlay.Breaks.First().StartTime); + AddAssert("test keys not counting", () => !scoreAccessiblePlayer.HUDOverlay.KeyCounter.IsCounting); + AddStep("rewind", () => scoreAccessiblePlayer.GameplayClockContainer.Seek(-80000)); + AddUntilStep("key counter reset", () => scoreAccessiblePlayer.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -43,6 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new HUDOverlay HUDOverlay => base.HUDOverlay; + public new BreakOverlay BreakOverlay => base.BreakOverlay; public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 11ca36e25f..237e364e69 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); - BreakOverlay.IsBreakTime.ValueChanged += _ => updatePauseOnFocusLostState(); + BreakOverlay.IsBreakTime.BindValueChanged(_ => onBreakTimeChanged(), true); } private void addUnderlayComponents(Container target) @@ -229,7 +229,7 @@ namespace osu.Game.Screens.Play IsPaused = { BindTarget = GameplayClockContainer.IsPaused } }, PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, - KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded } }, + KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, IsCounting = false }, RequestSeek = GameplayClockContainer.Seek, Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -286,6 +286,12 @@ namespace osu.Game.Screens.Play HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } + private void onBreakTimeChanged() + { + updatePauseOnFocusLostState(); + HUDOverlay.KeyCounter.IsCounting = !BreakOverlay.IsBreakTime.Value; + } + private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost && !DrawableRuleset.HasReplayLoaded.Value From 1ce972dd5b4a06d7ca6774a7daaf79cf3902dc69 Mon Sep 17 00:00:00 2001 From: naoey Date: Sat, 29 Feb 2020 21:53:49 +0530 Subject: [PATCH 009/387] Remove unused variable --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index ebd89039a5..8e16d74dca 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -14,8 +14,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Description("Player instantiated with an autoplay mod.")] public class TestSceneAutoplay : TestSceneAllRulesetPlayers { - private ClockBackedTestWorkingBeatmap.TrackVirtualManual track; - protected override Player CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); @@ -41,8 +39,6 @@ namespace osu.Game.Tests.Visual.Gameplay { var working = base.CreateWorkingBeatmap(beatmap, storyboard); - track = (ClockBackedTestWorkingBeatmap.TrackVirtualManual)working.Track; - return working; } From 089ec4c7922a18fe4f4da718bbc4ede63ddf6289 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sat, 29 Feb 2020 21:16:28 -0800 Subject: [PATCH 010/387] Test scene for mod development --- .../TestSceneCatchModSandbox.cs | 28 +++++ .../TestSceneManiaModSandbox.cs | 28 +++++ .../Mods/TestSceneOsuModDifficultyAdjust.cs | 20 ++++ .../TestSceneOsuModSandbox.cs | 28 +++++ .../TestSceneTaikoModSandbox.cs | 28 +++++ osu.Game/Tests/Visual/PlayerTestScene.cs | 4 +- osu.Game/Tests/Visual/TestSceneModSandbox.cs | 108 ++++++++++++++++++ 7 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs create mode 100644 osu.Game/Tests/Visual/TestSceneModSandbox.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs new file mode 100644 index 0000000000..3abf8163bd --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs @@ -0,0 +1,28 @@ +// 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 System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class TestSceneCatchModSandbox : TestSceneModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneCatchModSandbox)).ToList(); + + public TestSceneCatchModSandbox() + : this(null) + { + } + + public TestSceneCatchModSandbox(Mod mod = null) + : base(new CatchRuleset(), mod) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs new file mode 100644 index 0000000000..2693cebb43 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs @@ -0,0 +1,28 @@ +// 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 System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class TestSceneManiaModSandbox : TestSceneModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneManiaModSandbox)).ToList(); + + public TestSceneManiaModSandbox() + : this(null) + { + } + + public TestSceneManiaModSandbox(Mod mod = null) + : base(new ManiaRuleset(), mod) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs new file mode 100644 index 0000000000..7f09731a11 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -0,0 +1,20 @@ +// 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 System.Linq; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModDifficultyAdjust : TestSceneOsuModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(OsuModDifficultyAdjust)).ToList(); + + public TestSceneOsuModDifficultyAdjust() + : base(new OsuModDifficultyAdjust()) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs new file mode 100644 index 0000000000..d2a9d1ea6e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs @@ -0,0 +1,28 @@ +// 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 System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneOsuModSandbox : TestSceneModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneOsuModSandbox)).ToList(); + + public TestSceneOsuModSandbox() + : this(null) + { + } + + public TestSceneOsuModSandbox(Mod mod = null) + : base(new OsuRuleset(), mod) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs new file mode 100644 index 0000000000..f5481713f5 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs @@ -0,0 +1,28 @@ +// 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 System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TestSceneTaikoModSandbox : TestSceneModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneTaikoModSandbox)).ToList(); + + public TestSceneTaikoModSandbox() + : this(null) + { + } + + public TestSceneTaikoModSandbox(Mod mod = null) + : base(new TaikoRuleset(), mod) + { + } + } +} diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 7c5ba7d30f..1ca5256353 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual { base.SetUpSteps(); - AddStep(ruleset.RulesetInfo.Name, loadPlayer); + AddStep(ruleset.RulesetInfo.Name, LoadPlayer); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); } @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual protected virtual bool Autoplay => false; - private void loadPlayer() + protected void LoadPlayer() { var beatmap = CreateBeatmap(ruleset.RulesetInfo); diff --git a/osu.Game/Tests/Visual/TestSceneModSandbox.cs b/osu.Game/Tests/Visual/TestSceneModSandbox.cs new file mode 100644 index 0000000000..5c32ebadce --- /dev/null +++ b/osu.Game/Tests/Visual/TestSceneModSandbox.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; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + public abstract class TestSceneModSandbox : PlayerTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(TestSceneModSandbox) + }; + + protected Mod Mod; + private readonly TriangleButton button; + + protected TestSceneModSandbox(Ruleset ruleset, Mod mod = null) + : base(ruleset) + { + Mod = mod ?? new SandboxMod(); + + var props = Mod.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance); + var hasSettings = props.Any(prop => prop.GetCustomAttribute(true) != null); + + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(50), + Margin = new MarginPadding { Bottom = 20 }, + Width = 0.4f, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + new ModControlSection(Mod, Mod.CreateSettingsControls()), + button = new TriangleButton + { + RelativeSizeAxes = Axes.X, + Width = 0.5f, + Text = "Start", + Action = () => + { + button.Text = hasSettings ? "Apply Settings" : "Restart"; + LoadPlayer(); + } + } + } + }; + } + + [SetUpSteps] + public override void SetUpSteps() + { + } + + [BackgroundDependencyLoader] + private void load() + { + LocalConfig.GetBindable(OsuSetting.KeyOverlay).Value = true; + } + + protected override Player CreatePlayer(Ruleset ruleset) + { + SelectedMods.Value = SelectedMods.Value.Append(Mod).ToArray(); + + return base.CreatePlayer(ruleset); + } + + protected class SandboxMod : Mod + { + public override string Name => "Sandbox Test"; + public override string Acronym => "ST"; + public override double ScoreMultiplier => 1.0; + + [SettingSource("Test Setting")] + public Bindable TestSetting1 { get; } = new BindableBool + { + Default = true, + Value = true + }; + + [SettingSource("Test Setting 2")] + public Bindable TestSetting2 { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 0, + MaxValue = 20 + }; + } + } +} From a02c5710ac07a2486be020a7b0e2d1d4feb035b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 10:06:49 +0900 Subject: [PATCH 011/387] Rename base class --- osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs | 2 +- osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs | 2 +- .../{TestSceneModSandbox.cs => ModSandboxTestScene.cs} | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Tests/Visual/{TestSceneModSandbox.cs => ModSandboxTestScene.cs} (95%) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs index 3abf8163bd..3e94121fd7 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs @@ -11,7 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneCatchModSandbox : TestSceneModSandbox + public class TestSceneCatchModSandbox : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneCatchModSandbox)).ToList(); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs index 2693cebb43..897e7df1e9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs @@ -11,7 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests { [TestFixture] - public class TestSceneManiaModSandbox : TestSceneModSandbox + public class TestSceneManiaModSandbox : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneManiaModSandbox)).ToList(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs index d2a9d1ea6e..9f816ef2ed 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs @@ -11,7 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneOsuModSandbox : TestSceneModSandbox + public class TestSceneOsuModSandbox : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneOsuModSandbox)).ToList(); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs index f5481713f5..ef62c7ed56 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs @@ -11,7 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] - public class TestSceneTaikoModSandbox : TestSceneModSandbox + public class TestSceneTaikoModSandbox : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneTaikoModSandbox)).ToList(); diff --git a/osu.Game/Tests/Visual/TestSceneModSandbox.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs similarity index 95% rename from osu.Game/Tests/Visual/TestSceneModSandbox.cs rename to osu.Game/Tests/Visual/ModSandboxTestScene.cs index 5c32ebadce..bb872123c5 100644 --- a/osu.Game/Tests/Visual/TestSceneModSandbox.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -19,17 +19,17 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { - public abstract class TestSceneModSandbox : PlayerTestScene + public abstract class ModSandboxTestScene : PlayerTestScene { public override IReadOnlyList RequiredTypes => new[] { - typeof(TestSceneModSandbox) + typeof(ModSandboxTestScene) }; protected Mod Mod; private readonly TriangleButton button; - protected TestSceneModSandbox(Ruleset ruleset, Mod mod = null) + protected ModSandboxTestScene(Ruleset ruleset, Mod mod = null) : base(ruleset) { Mod = mod ?? new SandboxMod(); From 5c15704c819ab25868c48f511a26b417a2bb96d0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 10:28:39 +0900 Subject: [PATCH 012/387] Improve abstract structure for testability --- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 96 ++++++-------------- osu.Game/Tests/Visual/TestReplayPlayer.cs | 24 +++++ 2 files changed, 50 insertions(+), 70 deletions(-) create mode 100644 osu.Game/Tests/Visual/TestReplayPlayer.cs diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index bb872123c5..84bab6a9b9 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -4,15 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; -using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Mods; +using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; @@ -26,83 +18,47 @@ namespace osu.Game.Tests.Visual typeof(ModSandboxTestScene) }; - protected Mod Mod; - private readonly TriangleButton button; - - protected ModSandboxTestScene(Ruleset ruleset, Mod mod = null) + protected ModSandboxTestScene(Ruleset ruleset) : base(ruleset) { - Mod = mod ?? new SandboxMod(); - - var props = Mod.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance); - var hasSettings = props.Any(prop => prop.GetCustomAttribute(true) != null); - - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding(50), - Margin = new MarginPadding { Bottom = 20 }, - Width = 0.4f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Children = new Drawable[] - { - new ModControlSection(Mod, Mod.CreateSettingsControls()), - button = new TriangleButton - { - RelativeSizeAxes = Axes.X, - Width = 0.5f, - Text = "Start", - Action = () => - { - button.Text = hasSettings ? "Apply Settings" : "Restart"; - LoadPlayer(); - } - } - } - }; } - [SetUpSteps] + private ModTestCaseData currentTest; + public override void SetUpSteps() { + foreach (var testCase in CreateTestCases()) + { + AddStep("set test case", () => currentTest = testCase); + base.SetUpSteps(); + } } - [BackgroundDependencyLoader] - private void load() - { - LocalConfig.GetBindable(OsuSetting.KeyOverlay).Value = true; - } + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest.Beatmap; protected override Player CreatePlayer(Ruleset ruleset) { - SelectedMods.Value = SelectedMods.Value.Append(Mod).ToArray(); + SelectedMods.Value = SelectedMods.Value.Append(currentTest.Mod).ToArray(); + + if (currentTest.Autoplay) + { + // We're simulating an auto-play via a replay so that the auto-play mod does not interfere + var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value); + var score = ruleset.GetAutoplayMod().CreateReplayScore(beatmap); + + return new TestReplayPlayer(score, false, false); + } return base.CreatePlayer(ruleset); } - protected class SandboxMod : Mod + protected abstract ModTestCaseData[] CreateTestCases(); + + protected class ModTestCaseData { - public override string Name => "Sandbox Test"; - public override string Acronym => "ST"; - public override double ScoreMultiplier => 1.0; - - [SettingSource("Test Setting")] - public Bindable TestSetting1 { get; } = new BindableBool - { - Default = true, - Value = true - }; - - [SettingSource("Test Setting 2")] - public Bindable TestSetting2 { get; } = new BindableFloat - { - Precision = 0.1f, - MinValue = 0, - MaxValue = 20 - }; + public Mod Mod; + public bool Autoplay; + public IBeatmap Beatmap; } } } diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs new file mode 100644 index 0000000000..e99fcc1e37 --- /dev/null +++ b/osu.Game/Tests/Visual/TestReplayPlayer.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + public class TestReplayPlayer : ReplayPlayer + { + protected override bool PauseOnFocusLost { get; } + + public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; + + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + + public TestReplayPlayer(Score score, bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) + : base(score, allowPause, showResults) + { + PauseOnFocusLost = pauseOnFocusLost; + } + } +} From 239cfddcbb92503b662d89750953950b6a11a12d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 10:50:41 +0900 Subject: [PATCH 013/387] Improve test scenes/cases --- .../TestSceneCatchModSandbox.cs | 28 ------- .../TestSceneManiaModSandbox.cs | 28 ------- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 38 +++++++++- .../TestSceneOsuModSandbox.cs | 28 ------- .../TestSceneTaikoModSandbox.cs | 28 ------- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 73 +++++++++++++++---- 6 files changed, 95 insertions(+), 128 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs delete mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs delete mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs delete mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs deleted file mode 100644 index 3e94121fd7..0000000000 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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 System.Linq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Catch.Tests -{ - [TestFixture] - public class TestSceneCatchModSandbox : ModSandboxTestScene - { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneCatchModSandbox)).ToList(); - - public TestSceneCatchModSandbox() - : this(null) - { - } - - public TestSceneCatchModSandbox(Mod mod = null) - : base(new CatchRuleset(), mod) - { - } - } -} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs deleted file mode 100644 index 897e7df1e9..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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 System.Linq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Mania.Tests -{ - [TestFixture] - public class TestSceneManiaModSandbox : ModSandboxTestScene - { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneManiaModSandbox)).ToList(); - - public TestSceneManiaModSandbox() - : this(null) - { - } - - public TestSceneManiaModSandbox(Mod mod = null) - : base(new ManiaRuleset(), mod) - { - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 7f09731a11..46a3c1dff3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -5,16 +5,50 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModDifficultyAdjust : TestSceneOsuModSandbox + public class TestSceneOsuModDifficultyAdjust : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(OsuModDifficultyAdjust)).ToList(); public TestSceneOsuModDifficultyAdjust() - : base(new OsuModDifficultyAdjust()) + : base(new OsuRuleset()) { } + + protected override ModTestCaseData[] CreateTestCases() => new[] + { + new ModTestCaseData("no adjustment", new OsuModDifficultyAdjust()) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }, + new ModTestCaseData("cs = 10", new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }, + new ModTestCaseData("ar = 10", new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }, + }; + + protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); + + private class ScoreAccessibleTestPlayer : TestPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public ScoreAccessibleTestPlayer(Score score) + : base(score) + { + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs deleted file mode 100644 index 9f816ef2ed..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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 System.Linq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Osu.Tests -{ - [TestFixture] - public class TestSceneOsuModSandbox : ModSandboxTestScene - { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneOsuModSandbox)).ToList(); - - public TestSceneOsuModSandbox() - : this(null) - { - } - - public TestSceneOsuModSandbox(Mod mod = null) - : base(new OsuRuleset(), mod) - { - } - } -} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs deleted file mode 100644 index ef62c7ed56..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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 System.Linq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Taiko.Tests -{ - [TestFixture] - public class TestSceneTaikoModSandbox : ModSandboxTestScene - { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneTaikoModSandbox)).ToList(); - - public TestSceneTaikoModSandbox() - : this(null) - { - } - - public TestSceneTaikoModSandbox(Mod mod = null) - : base(new TaikoRuleset(), mod) - { - } - } -} diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index 84bab6a9b9..0610a145ae 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Game.Beatmaps; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual @@ -29,36 +32,78 @@ namespace osu.Game.Tests.Visual { foreach (var testCase in CreateTestCases()) { - AddStep("set test case", () => currentTest = testCase); + AddStep(testCase.Name, () => currentTest = testCase); base.SetUpSteps(); + AddUntilStep("test passed", () => testCase.PassCondition?.Invoke() ?? true); } } - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest.Beatmap; + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest?.Beatmap ?? base.CreateBeatmap(ruleset); - protected override Player CreatePlayer(Ruleset ruleset) + protected sealed override Player CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Append(currentTest.Mod).ToArray(); - if (currentTest.Autoplay) - { - // We're simulating an auto-play via a replay so that the auto-play mod does not interfere - var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value); - var score = ruleset.GetAutoplayMod().CreateReplayScore(beatmap); + var score = currentTest.Autoplay + ? ruleset.GetAutoplayMod().CreateReplayScore(Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value)) + : new Score { Replay = new Replay() }; - return new TestReplayPlayer(score, false, false); - } - - return base.CreatePlayer(ruleset); + return CreateReplayPlayer(score); } + /// + /// Creates the test cases for this test scene. + /// protected abstract ModTestCaseData[] CreateTestCases(); + /// + /// Creates the for a test case. + /// + /// The . + protected virtual TestPlayer CreateReplayPlayer(Score score) => new TestPlayer(score); + + protected class TestPlayer : TestReplayPlayer + { + public TestPlayer(Score score) + : base(score, false, false) + { + } + } + protected class ModTestCaseData { - public Mod Mod; - public bool Autoplay; + /// + /// Whether to use a replay to simulate an auto-play. True by default. + /// + public bool Autoplay = true; + + /// + /// The beatmap for this test case. + /// + [CanBeNull] public IBeatmap Beatmap; + + /// + /// The conditions that cause this test case to pass. + /// + [CanBeNull] + public Func PassCondition; + + /// + /// The name of this test case, displayed in the test browser. + /// + public readonly string Name; + + /// + /// The this test case tests. + /// + public readonly Mod Mod; + + public ModTestCaseData(string name, Mod mod) + { + Name = name; + Mod = mod; + } } } } From ce7cbf29ca1546f4fc8f1d81ca8cbb401537dfdf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 12:20:25 +0900 Subject: [PATCH 014/387] Move to using test methods for better separation --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 37 ++++++++++--------- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 22 +++++------ osu.Game/Tests/Visual/PlayerTestScene.cs | 17 +++++++++ osu.Game/Tests/Visual/ScreenTestScene.cs | 2 +- 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 46a3c1dff3..1fc9ccccd1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -20,24 +21,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { } - protected override ModTestCaseData[] CreateTestCases() => new[] + [Test] + public void TestNoAdjustment() => CreateModTest(new ModTestCaseData("no adjustment", new OsuModDifficultyAdjust()) { - new ModTestCaseData("no adjustment", new OsuModDifficultyAdjust()) - { - Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 - }, - new ModTestCaseData("cs = 10", new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) - { - Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 - }, - new ModTestCaseData("ar = 10", new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) - { - Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 - }, - }; + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }); + + [Test] + public void TestCircleSize10() => CreateModTest(new ModTestCaseData("cs = 10", new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }); + + [Test] + public void TestApproachRate10() => CreateModTest(new ModTestCaseData("ar = 10", new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }); protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index 0610a145ae..a1fa757452 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual { public abstract class ModSandboxTestScene : PlayerTestScene { + protected sealed override bool HasCustomSteps => true; + public override IReadOnlyList RequiredTypes => new[] { typeof(ModSandboxTestScene) @@ -28,14 +30,15 @@ namespace osu.Game.Tests.Visual private ModTestCaseData currentTest; - public override void SetUpSteps() + protected void CreateModTest(ModTestCaseData testCaseData) => CreateTest(() => { - foreach (var testCase in CreateTestCases()) - { - AddStep(testCase.Name, () => currentTest = testCase); - base.SetUpSteps(); - AddUntilStep("test passed", () => testCase.PassCondition?.Invoke() ?? true); - } + AddStep("set test data", () => currentTest = testCaseData); + }); + + public override void TearDownSteps() + { + AddUntilStep("test passed", () => currentTest?.PassCondition?.Invoke() ?? true); + base.TearDownSteps(); } protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest?.Beatmap ?? base.CreateBeatmap(ruleset); @@ -51,11 +54,6 @@ namespace osu.Game.Tests.Visual return CreateReplayPlayer(score); } - /// - /// Creates the test cases for this test scene. - /// - protected abstract ModTestCaseData[] CreateTestCases(); - /// /// Creates the for a test case. /// diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 1ca5256353..0d5aac8cfd 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.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 System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -14,6 +15,11 @@ namespace osu.Game.Tests.Visual { public abstract class PlayerTestScene : RateAdjustedBeatmapTestScene { + /// + /// Whether custom test steps are provided. Custom tests should invoke to create the test steps. + /// + protected virtual bool HasCustomSteps { get; } = false; + private readonly Ruleset ruleset; protected Player Player; @@ -37,6 +43,17 @@ namespace osu.Game.Tests.Visual { base.SetUpSteps(); + if (!HasCustomSteps) + CreateTest(null); + } + + protected void CreateTest(Action action) + { + if (action != null && !HasCustomSteps) + throw new InvalidOperationException($"Cannot add custom test steps without {nameof(HasCustomSteps)} being set."); + + action?.Invoke(); + AddStep(ruleset.RulesetInfo.Name, LoadPlayer); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); } diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index feca592049..d26aacf2bc 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual public virtual void SetUpSteps() => addExitAllScreensStep(); [TearDownSteps] - public void TearDownSteps() => addExitAllScreensStep(); + public virtual void TearDownSteps() => addExitAllScreensStep(); private void addExitAllScreensStep() { From 6d939e9d415609f7d64dea9874456529d32b48f4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 12:42:48 +0900 Subject: [PATCH 015/387] Add failing test scenes --- .../TestSceneCatchModPerfect.cs | 54 +++++++++++++++ .../TestSceneManiaModPerfect.cs | 26 ++++++++ .../TestSceneOsuModPerfect.cs | 52 +++++++++++++++ .../TestSceneTaikoModPerfect.cs | 30 +++++++++ osu.Game/Tests/Visual/ModPerfectTestScene.cs | 66 +++++++++++++++++++ 5 files changed, 228 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs create mode 100644 osu.Game/Tests/Visual/ModPerfectTestScene.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs new file mode 100644 index 0000000000..f5bd3b1133 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneCatchModPerfect : ModPerfectTestScene + { + public TestSceneCatchModPerfect() + : base(new CatchRuleset(), new CatchModPerfect()) + { + } + + [TestCase(false)] + [TestCase(true)] + public void TestBananaShower(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new BananaShower { StartTime = 1000, EndTime = 3000 }, false), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestFruit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Fruit { StartTime = 1000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestTestJuiceStream(bool shouldMiss) + { + var stream = new JuiceStream + { + StartTime = 1000, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(100, 0), + }) + }; + + CreateHitObjectTest(new HitObjectTestCase(stream), shouldMiss); + } + + // We only care about testing misses, hits are tested via JuiceStream + [TestCase(true)] + public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Droplet { StartTime = 1000 }), shouldMiss); + + // We only care about testing misses, hits are tested via JuiceStream + [TestCase(true)] + public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new TinyDroplet { StartTime = 1000 }), shouldMiss); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs new file mode 100644 index 0000000000..f49a19e218 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneManiaModPerfect : ModPerfectTestScene + { + public TestSceneManiaModPerfect() + : base(new ManiaRuleset(), new ManiaModPerfect()) + { + } + + [TestCase(false)] + [TestCase(true)] + public void TestNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Note { StartTime = 1000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs new file mode 100644 index 0000000000..02fd5b5a79 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneOsuModPerfect : ModPerfectTestScene + { + public TestSceneOsuModPerfect() + : base(new OsuRuleset(), new OsuModPerfect()) + { + } + + [TestCase(false)] + [TestCase(true)] + public void TestHitCircle(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new HitCircle { StartTime = 1000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestSlider(bool shouldMiss) + { + var slider = new Slider + { + StartTime = 1000, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + }; + + CreateHitObjectTest(new HitObjectTestCase(slider), shouldMiss); + } + + [TestCase(false)] + [TestCase(true)] + public void TestSpinner(bool shouldMiss) + { + var spinner = new Spinner + { + StartTime = 1000, + EndTime = 3000, + Position = new Vector2(256, 192) + }; + + CreateHitObjectTest(new HitObjectTestCase(spinner), shouldMiss); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs new file mode 100644 index 0000000000..4069ee7983 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneTaikoModPerfect : ModPerfectTestScene + { + public TestSceneTaikoModPerfect() + : base(new TaikoRuleset(), new TaikoModPerfect()) + { + } + + [TestCase(false)] + [TestCase(true)] + public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new CentreHit { StartTime = 1000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new DrumRoll { StartTime = 1000, EndTime = 3000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); + } +} diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs new file mode 100644 index 0000000000..272b5366a9 --- /dev/null +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -0,0 +1,66 @@ +// 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.Extensions.TypeExtensions; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Scoring; + +namespace osu.Game.Tests.Visual +{ + public abstract class ModPerfectTestScene : ModSandboxTestScene + { + private readonly ModPerfect perfectMod; + + protected ModPerfectTestScene(Ruleset ruleset, ModPerfect perfectMod) + : base(ruleset) + { + this.perfectMod = perfectMod; + } + + protected void CreateHitObjectTest(HitObjectTestCase testCaseData, bool shouldMiss) => CreateModTest(new ModTestCaseData(testCaseData.HitObject.GetType().ReadableName(), perfectMod) + { + Beatmap = new Beatmap + { + BeatmapInfo = { Ruleset = Ruleset.Value }, + HitObjects = { testCaseData.HitObject } + }, + Autoplay = !shouldMiss, + PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testCaseData.FailOnMiss) + }); + + protected sealed override TestPlayer CreateReplayPlayer(Score score) => new PerfectModTestPlayer(score); + + private class PerfectModTestPlayer : TestPlayer + { + public PerfectModTestPlayer(Score score) + : base(score) + { + } + + protected override bool AllowFail => true; + + public bool CheckFailed(bool failed) + { + if (!failed) + return ScoreProcessor.HasCompleted && !HealthProcessor.HasFailed; + + return ScoreProcessor.JudgedHits > 0 && HealthProcessor.HasFailed; + } + } + + protected class HitObjectTestCase + { + public readonly HitObject HitObject; + public readonly bool FailOnMiss; + + public HitObjectTestCase(HitObject hitObject, bool failOnMiss = true) + { + HitObject = hitObject; + FailOnMiss = failOnMiss; + } + } + } +} From e801ad514bacd08de9b09f4669278ac51a777223 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:24:02 +0900 Subject: [PATCH 016/387] Fix ruleset nullref --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 272b5366a9..ded1b3676e 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -12,11 +12,13 @@ namespace osu.Game.Tests.Visual { public abstract class ModPerfectTestScene : ModSandboxTestScene { + private readonly Ruleset ruleset; private readonly ModPerfect perfectMod; protected ModPerfectTestScene(Ruleset ruleset, ModPerfect perfectMod) : base(ruleset) { + this.ruleset = ruleset; this.perfectMod = perfectMod; } @@ -24,7 +26,7 @@ namespace osu.Game.Tests.Visual { Beatmap = new Beatmap { - BeatmapInfo = { Ruleset = Ruleset.Value }, + BeatmapInfo = { Ruleset = ruleset.RulesetInfo }, HitObjects = { testCaseData.HitObject } }, Autoplay = !shouldMiss, From cd43a0c9e8071c818fa289972076ff61fcc1b5c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:24:07 +0900 Subject: [PATCH 017/387] Better check for failing state --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index ded1b3676e..31d2ce9281 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual if (!failed) return ScoreProcessor.HasCompleted && !HealthProcessor.HasFailed; - return ScoreProcessor.JudgedHits > 0 && HealthProcessor.HasFailed; + return HealthProcessor.HasFailed; } } From bb4193d985cb47e450adafe03059b03fcff38705 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:24:55 +0900 Subject: [PATCH 018/387] Fix taiko infinity health drain on some beatmaps --- osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index edb089dbac..dd3c2289ea 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.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 System; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); } From e58fb3f52823a4feb1ead00097000e4e689cac48 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:25:16 +0900 Subject: [PATCH 019/387] Make default taiko HP 1 for test scene --- .../TestSceneTaikoModPerfect.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs index 4069ee7983..b67810846e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests @@ -11,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Tests public class TestSceneTaikoModPerfect : ModPerfectTestScene { public TestSceneTaikoModPerfect() - : base(new TaikoRuleset(), new TaikoModPerfect()) + : base(new TestTaikoRuleset(), new TaikoModPerfect()) { } @@ -26,5 +28,20 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestCase(false)] [TestCase(true)] public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); + + private class TestTaikoRuleset : TaikoRuleset + { + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TestTaikoHealthProcessor(); + + private class TestTaikoHealthProcessor : TaikoHealthProcessor + { + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + Health.Value = 1; // Don't care about the health condition (only the mod condition) + } + } + } } } From 6fb52e5370c67de6a5f67bff3d8f19b13dc5bc22 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:25:36 +0900 Subject: [PATCH 020/387] Fix custom rulesets not being testable --- osu.Game/Tests/Visual/PlayerTestScene.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 0d5aac8cfd..3d8eefaa9d 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -67,6 +67,7 @@ namespace osu.Game.Tests.Visual var beatmap = CreateBeatmap(ruleset.RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(beatmap); + Ruleset.Value = ruleset.RulesetInfo; if (!AllowFail) { From 6d051d9e42599c694d9a989c3c6a897451d968b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:25:56 +0900 Subject: [PATCH 021/387] Fix perfect mod failure cases --- osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs | 6 ++++++ osu.Game/Rulesets/Mods/ModPerfect.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs index fb92399102..e3391c47f1 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs @@ -1,11 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Mods { public class CatchModPerfect : ModPerfect { + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + => !(result.Judgement is CatchBananaJudgement) + && base.FailCondition(healthProcessor, result); } } diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 882d3ebd6a..7fe606d584 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModPerfect; public override string Description => "SS or quit."; - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => result.Type != result.Judgement.MaxResult; + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + => !(result.Judgement is IgnoreJudgement) + && result.Type != result.Judgement.MaxResult; } } From 3d344a076de0a934a5adfb92f2d1d01a3e1da31d Mon Sep 17 00:00:00 2001 From: naoey Date: Tue, 3 Mar 2020 06:17:25 +0530 Subject: [PATCH 022/387] Add test for disabled keycounter, don't discard change event values --- .../Visual/Gameplay/TestSceneKeyCounter.cs | 23 ++++++++++--------- osu.Game/Screens/Play/Player.cs | 19 ++++++++------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index e7b3e007fc..10827bc0b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -47,21 +47,22 @@ namespace osu.Game.Tests.Visual.Gameplay Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; - AddStep($"Press {testKey} key", () => + void addPressKeyStep() { - InputManager.PressKey(testKey); - InputManager.ReleaseKey(testKey); - }); + AddStep($"Press {testKey} key", () => + { + InputManager.PressKey(testKey); + InputManager.ReleaseKey(testKey); + }); + } + addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); - - AddStep($"Press {testKey} key", () => - { - InputManager.PressKey(testKey); - InputManager.ReleaseKey(testKey); - }); - + addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2); + AddStep($"Disable counting", () => testCounter.IsCounting = false); + addPressKeyStep(); + AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2); Add(kc); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 237e364e69..2d49c707ec 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -157,7 +157,10 @@ namespace osu.Game.Screens.Play addGameplayComponents(GameplayClockContainer, Beatmap.Value); addOverlayComponents(GameplayClockContainer, Beatmap.Value); - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); + DrawableRuleset.HasReplayLoaded.BindValueChanged(e => + { + updatePauseOnFocusLostState(e.NewValue, BreakOverlay.IsBreakTime.Value); + }, true); // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); @@ -184,7 +187,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); - BreakOverlay.IsBreakTime.BindValueChanged(_ => onBreakTimeChanged(), true); + BreakOverlay.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } private void addUnderlayComponents(Container target) @@ -286,16 +289,16 @@ namespace osu.Game.Screens.Play HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } - private void onBreakTimeChanged() + private void onBreakTimeChanged(ValueChangedEvent changeEvent) { - updatePauseOnFocusLostState(); - HUDOverlay.KeyCounter.IsCounting = !BreakOverlay.IsBreakTime.Value; + updatePauseOnFocusLostState(DrawableRuleset.HasReplayLoaded.Value, changeEvent.NewValue); + HUDOverlay.KeyCounter.IsCounting = !changeEvent.NewValue; } - private void updatePauseOnFocusLostState() => + private void updatePauseOnFocusLostState(bool replayLoaded, bool isBreakTime) => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost - && !DrawableRuleset.HasReplayLoaded.Value - && !BreakOverlay.IsBreakTime.Value; + && !replayLoaded + && !isBreakTime; private IBeatmap loadPlayableBeatmap() { From 90c2f7bd89620bc05cd0529f3a74780199e2fa0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 12:13:36 +0900 Subject: [PATCH 023/387] Fail tests by default --- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index a1fa757452..8bfa373e46 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual public override void TearDownSteps() { - AddUntilStep("test passed", () => currentTest?.PassCondition?.Invoke() ?? true); + AddUntilStep("test passed", () => currentTest?.PassCondition?.Invoke() ?? false); base.TearDownSteps(); } From 1e26df64b6cc022123222e4e104fbb382670037d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 12:58:07 +0900 Subject: [PATCH 024/387] Fix constructor test failures --- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index 8bfa373e46..11612d0eca 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -37,7 +37,14 @@ namespace osu.Game.Tests.Visual public override void TearDownSteps() { - AddUntilStep("test passed", () => currentTest?.PassCondition?.Invoke() ?? false); + AddUntilStep("test passed", () => + { + if (currentTest == null) + return true; + + return currentTest.PassCondition?.Invoke() ?? false; + }); + base.TearDownSteps(); } From a1aecd4c3905bbd9cd9d9e1925d257ff40e6419a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 12:59:41 +0900 Subject: [PATCH 025/387] Fix TrackVirtualManual not respecting rate adjustments --- osu.Game/Tests/Visual/OsuTestScene.cs | 57 ++++++--------------------- 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index b203557fab..3faecc87d2 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -229,17 +229,10 @@ namespace osu.Game.Tests.Visual /// public class TrackVirtualManual : Track { + private readonly StopwatchClock stopwatchClock = new StopwatchClock(); + private readonly IFrameBasedClock referenceClock; - private readonly ManualClock clock = new ManualClock(); - - private bool running; - - /// - /// Local offset added to the reference clock to resolve correct time. - /// - private double offset; - public TrackVirtualManual(IFrameBasedClock referenceClock) { this.referenceClock = referenceClock; @@ -248,60 +241,32 @@ namespace osu.Game.Tests.Visual public override bool Seek(double seek) { - offset = Math.Clamp(seek, 0, Length); - lastReferenceTime = null; + var offset = Math.Clamp(seek, 0, Length); + + stopwatchClock.Seek(offset); return offset == seek; } - public override void Start() - { - running = true; - } + public override void Start() => stopwatchClock.Start(); public override void Reset() { - Seek(0); + stopwatchClock.Seek(0); base.Reset(); } - public override void Stop() - { - if (running) - { - running = false; - // on stopping, the current value should be transferred out of the clock, as we can no longer rely on - // the referenceClock (which will still be counting time). - offset = clock.CurrentTime; - lastReferenceTime = null; - } - } + public override void Stop() => stopwatchClock.Stop(); - public override bool IsRunning => running; + public override bool IsRunning => stopwatchClock.IsRunning; - private double? lastReferenceTime; - - public override double CurrentTime => clock.CurrentTime; + public override double CurrentTime => stopwatchClock.CurrentTime; protected override void UpdateState() { base.UpdateState(); - if (running) - { - double refTime = referenceClock.CurrentTime; - - if (!lastReferenceTime.HasValue) - { - // if the clock just started running, the current value should be transferred to the offset - // (to zero the progression of time). - offset -= refTime; - } - - lastReferenceTime = refTime; - } - - clock.CurrentTime = Math.Min((lastReferenceTime ?? 0) + offset, Length); + stopwatchClock.Rate = Rate * referenceClock.Rate; if (CurrentTime >= Length) { From cc5b44e46681eb6b46c6c0fc1109cd5b64dd108b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 13:36:55 +0900 Subject: [PATCH 026/387] Add test scene --- .../Mods/TestSceneOsuModDoubleTime.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs new file mode 100644 index 0000000000..deb733c581 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModDoubleTime : ModSandboxTestScene + { + public TestSceneOsuModDoubleTime() + : base(new OsuRuleset()) + { + } + + [TestCase(0.5)] + [TestCase(1.01)] + [TestCase(1.5)] + [TestCase(2)] + [TestCase(5)] + public void TestDefaultRate(double rate) => CreateModTest(new ModTestCaseData("1.5x", new OsuModDoubleTime { SpeedChange = { Value = rate } }) + { + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }); + + protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); + + private class ScoreAccessibleTestPlayer : TestPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public ScoreAccessibleTestPlayer(Score score) + : base(score) + { + } + } + } +} From d11d29c1f7303988baad64f2efcb4979af6425db Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 15:30:53 +0900 Subject: [PATCH 027/387] Adjust namespaces --- .../{ => Mods}/TestSceneCatchModPerfect.cs | 2 +- .../{ => Mods}/TestSceneManiaModPerfect.cs | 2 +- .../{ => Mods}/TestSceneOsuModPerfect.cs | 2 +- .../{ => Mods}/TestSceneTaikoModPerfect.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game.Rulesets.Catch.Tests/{ => Mods}/TestSceneCatchModPerfect.cs (97%) rename osu.Game.Rulesets.Mania.Tests/{ => Mods}/TestSceneManiaModPerfect.cs (95%) rename osu.Game.Rulesets.Osu.Tests/{ => Mods}/TestSceneOsuModPerfect.cs (97%) rename osu.Game.Rulesets.Taiko.Tests/{ => Mods}/TestSceneTaikoModPerfect.cs (97%) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs similarity index 97% rename from osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs rename to osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index f5bd3b1133..3e28bac02f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Catch.Tests +namespace osu.Game.Rulesets.Catch.Tests.Mods { public class TestSceneCatchModPerfect : ModPerfectTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs similarity index 95% rename from osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs rename to osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index f49a19e218..4e11a302c9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Mods { public class TestSceneManiaModPerfect : ModPerfectTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs similarity index 97% rename from osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs rename to osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index 02fd5b5a79..fc2dfa16ec 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModPerfect : ModPerfectTestScene { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs similarity index 97% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs rename to osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index b67810846e..fd9d01a3db 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Mods { public class TestSceneTaikoModPerfect : ModPerfectTestScene { From a26ac31c64076d68335ba53018132eb860907101 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 15:33:54 +0900 Subject: [PATCH 028/387] Fix test name --- osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 3e28bac02f..56d2fe1ee0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestTestJuiceStream(bool shouldMiss) + public void TestJuiceStream(bool shouldMiss) { var stream = new JuiceStream { From 8dcdd6db6fc39b1e33a65675a5eb4c8cdccde8e5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 16:20:36 +0300 Subject: [PATCH 029/387] Rename UpdateStream components to ChangelogUpdateStream --- .../Visual/Online/TestSceneChangelogOverlay.cs | 4 ++-- osu.Game/Overlays/Changelog/ChangelogHeader.cs | 4 ++-- ...ateStreamBadge.cs => ChangelogUpdateStreamBadge.cs} | 4 ++-- ...mBadgeArea.cs => ChangelogUpdateStreamBadgeArea.cs} | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Overlays/Changelog/{UpdateStreamBadge.cs => ChangelogUpdateStreamBadge.cs} (97%) rename osu.Game/Overlays/Changelog/{UpdateStreamBadgeArea.cs => ChangelogUpdateStreamBadgeArea.cs} (84%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 7a8570c09b..530a486de8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -17,8 +17,8 @@ namespace osu.Game.Tests.Visual.Online public override IReadOnlyList RequiredTypes => new[] { - typeof(UpdateStreamBadgeArea), - typeof(UpdateStreamBadge), + typeof(ChangelogUpdateStreamBadgeArea), + typeof(ChangelogUpdateStreamBadge), typeof(ChangelogHeader), typeof(ChangelogContent), typeof(ChangelogListing), diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index dcadbf4cf5..0667bedfc6 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Changelog public Action ListingSelected; - public UpdateStreamBadgeArea Streams; + public ChangelogUpdateStreamBadgeArea Streams; private const string listing_string = "listing"; @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Changelog Horizontal = 65, Vertical = 20 }, - Child = Streams = new UpdateStreamBadgeArea() + Child = Streams = new ChangelogUpdateStreamBadgeArea() } } }; diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs similarity index 97% rename from osu.Game/Overlays/Changelog/UpdateStreamBadge.cs rename to osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs index 6786bbc49f..20cc564013 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs @@ -17,7 +17,7 @@ using osuTK; namespace osu.Game.Overlays.Changelog { - public class UpdateStreamBadge : TabItem + public class ChangelogUpdateStreamBadge : TabItem { private const float badge_width = 100; private const float transition_duration = 100; @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Changelog private FillFlowContainer text; private ExpandingBar expandingBar; - public UpdateStreamBadge(APIUpdateStream stream) + public ChangelogUpdateStreamBadge(APIUpdateStream stream) : base(stream) { this.stream = stream; diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs similarity index 84% rename from osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs rename to osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs index ffb622dd37..5ab86a72f3 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs @@ -10,9 +10,9 @@ using osu.Framework.Graphics.UserInterface; namespace osu.Game.Overlays.Changelog { - public class UpdateStreamBadgeArea : TabControl + public class ChangelogUpdateStreamBadgeArea : TabControl { - public UpdateStreamBadgeArea() + public ChangelogUpdateStreamBadgeArea() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Changelog protected override bool OnHover(HoverEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType()) + foreach (var streamBadge in TabContainer.Children.OfType()) streamBadge.UserHoveringArea = true; return base.OnHover(e); @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Changelog protected override void OnHoverLost(HoverLostEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType()) + foreach (var streamBadge in TabContainer.Children.OfType()) streamBadge.UserHoveringArea = false; base.OnHoverLost(e); @@ -50,6 +50,6 @@ namespace osu.Game.Overlays.Changelog protected override Dropdown CreateDropdown() => null; protected override TabItem CreateTabItem(APIUpdateStream value) => - new UpdateStreamBadge(value) { SelectedTab = { BindTarget = Current } }; + new ChangelogUpdateStreamBadge(value) { SelectedTab = { BindTarget = Current } }; } } From 937d9da43b183104f4cca9a4de427922a960bf37 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 17:01:58 +0300 Subject: [PATCH 030/387] Implement OverlayUpdateStreamControl component --- .../Online/TestSceneChangelogOverlay.cs | 4 +- .../Overlays/Changelog/ChangelogHeader.cs | 4 +- .../Changelog/ChangelogUpdateStreamControl.cs | 12 +++ .../Changelog/ChangelogUpdateStreamItem.cs | 25 ++++++ ...eArea.cs => OverlayUpdateStreamControl.cs} | 53 ++++++------ ...eamBadge.cs => OverlayUpdateStreamItem.cs} | 80 ++++++++++--------- 6 files changed, 111 insertions(+), 67 deletions(-) create mode 100644 osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs create mode 100644 osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs rename osu.Game/Overlays/{Changelog/ChangelogUpdateStreamBadgeArea.cs => OverlayUpdateStreamControl.cs} (61%) rename osu.Game/Overlays/{Changelog/ChangelogUpdateStreamBadge.cs => OverlayUpdateStreamItem.cs} (78%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 530a486de8..864fd31a0f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -17,8 +17,8 @@ namespace osu.Game.Tests.Visual.Online public override IReadOnlyList RequiredTypes => new[] { - typeof(ChangelogUpdateStreamBadgeArea), - typeof(ChangelogUpdateStreamBadge), + typeof(ChangelogUpdateStreamControl), + typeof(ChangelogUpdateStreamItem), typeof(ChangelogHeader), typeof(ChangelogContent), typeof(ChangelogListing), diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 0667bedfc6..532efeb4bd 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Changelog public Action ListingSelected; - public ChangelogUpdateStreamBadgeArea Streams; + public ChangelogUpdateStreamControl Streams; private const string listing_string = "listing"; @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Changelog Horizontal = 65, Vertical = 20 }, - Child = Streams = new ChangelogUpdateStreamBadgeArea() + Child = Streams = new ChangelogUpdateStreamControl() } } }; diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs new file mode 100644 index 0000000000..555f0904d6 --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs @@ -0,0 +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 osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogUpdateStreamControl : OverlayUpdateStreamControl + { + protected override OverlayUpdateStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs new file mode 100644 index 0000000000..6a4801bc4b --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Humanizer; +using osu.Game.Online.API.Requests.Responses; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogUpdateStreamItem : OverlayUpdateStreamItem + { + public ChangelogUpdateStreamItem(APIUpdateStream stream) + : base(stream) + { + } + + protected override string GetMainText() => Value.DisplayName; + + protected override string GetAdditionalText() => Value.LatestBuild.DisplayVersion; + + protected override string GetInfoText() => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + + protected override Color4 GetBarColour() => Value.Colour; + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs b/osu.Game/Overlays/OverlayUpdateStreamControl.cs similarity index 61% rename from osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs rename to osu.Game/Overlays/OverlayUpdateStreamControl.cs index 5ab86a72f3..0fdf6c0111 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamControl.cs @@ -3,42 +3,32 @@ using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Online.API.Requests.Responses; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.UserInterface; +using JetBrains.Annotations; -namespace osu.Game.Overlays.Changelog +namespace osu.Game.Overlays { - public class ChangelogUpdateStreamBadgeArea : TabControl + public abstract class OverlayUpdateStreamControl : TabControl { - public ChangelogUpdateStreamBadgeArea() + protected OverlayUpdateStreamControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; } - public void Populate(List streams) + public void Populate(List streams) => streams.ForEach(AddItem); + + protected override Dropdown CreateDropdown() => null; + + protected override TabItem CreateTabItem(T value) => CreateStreamItem(value).With(item => { - foreach (var updateStream in streams) - AddItem(updateStream); - } + item.SelectedItem.BindTo(Current); + }); - protected override bool OnHover(HoverEvent e) - { - foreach (var streamBadge in TabContainer.Children.OfType()) - streamBadge.UserHoveringArea = true; - - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - foreach (var streamBadge in TabContainer.Children.OfType()) - streamBadge.UserHoveringArea = false; - - base.OnHoverLost(e); - } + [NotNull] + protected abstract OverlayUpdateStreamItem CreateStreamItem(T value); protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { @@ -47,9 +37,20 @@ namespace osu.Game.Overlays.Changelog AllowMultiline = true, }; - protected override Dropdown CreateDropdown() => null; + protected override bool OnHover(HoverEvent e) + { + foreach (var streamBadge in TabContainer.Children.OfType>()) + streamBadge.UserHoveringArea = true; - protected override TabItem CreateTabItem(APIUpdateStream value) => - new ChangelogUpdateStreamBadge(value) { SelectedTab = { BindTarget = Current } }; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + foreach (var streamBadge in TabContainer.Children.OfType>()) + streamBadge.UserHoveringArea = false; + + base.OnHoverLost(e); + } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs similarity index 78% rename from osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs rename to osu.Game/Overlays/OverlayUpdateStreamItem.cs index 20cc564013..5014aac5b0 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -1,44 +1,54 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using Humanizer; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.Sprites; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Allocation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics; using osuTK; +using osuTK.Graphics; -namespace osu.Game.Overlays.Changelog +namespace osu.Game.Overlays { - public class ChangelogUpdateStreamBadge : TabItem + public abstract class OverlayUpdateStreamItem : TabItem { - private const float badge_width = 100; private const float transition_duration = 100; + private const float tab_width = 100; - public readonly Bindable SelectedTab = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); - private readonly APIUpdateStream stream; + private bool userHoveringArea; + + public bool UserHoveringArea + { + set + { + if (value == userHoveringArea) + return; + + userHoveringArea = value; + updateState(); + } + } private FillFlowContainer text; private ExpandingBar expandingBar; - public ChangelogUpdateStreamBadge(APIUpdateStream stream) - : base(stream) + public OverlayUpdateStreamItem(T value) + : base(value) { - this.stream = stream; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Size = new Vector2(stream.IsFeatured ? badge_width * 2 : badge_width, 60); + Size = new Vector2(GetWidth(), 60); Padding = new MarginPadding(5); AddRange(new Drawable[] @@ -52,17 +62,17 @@ namespace osu.Game.Overlays.Changelog { new OsuSpriteText { - Text = stream.DisplayName, + Text = GetMainText(), Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), }, new OsuSpriteText { - Text = stream.LatestBuild.DisplayVersion, + Text = GetAdditionalText(), Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), }, new OsuSpriteText { - Text = stream.LatestBuild.Users > 0 ? $"{"user".ToQuantity(stream.LatestBuild.Users, "N0")} online" : null, + Text = GetInfoText(), Font = OsuFont.GetFont(size: 10), Colour = colourProvider.Foreground1 }, @@ -71,7 +81,7 @@ namespace osu.Game.Overlays.Changelog expandingBar = new ExpandingBar { Anchor = Anchor.TopCentre, - Colour = stream.Colour, + Colour = GetBarColour(), ExpandedSize = 4, CollapsedSize = 2, Expanded = true @@ -79,9 +89,19 @@ namespace osu.Game.Overlays.Changelog new HoverClickSounds() }); - SelectedTab.BindValueChanged(_ => updateState(), true); + SelectedItem.BindValueChanged(_ => updateState(), true); } + protected abstract string GetMainText(); + + protected abstract string GetAdditionalText(); + + protected virtual string GetInfoText() => string.Empty; + + protected abstract Color4 GetBarColour(); + + protected virtual float GetWidth() => tab_width; + protected override void OnActivated() => updateState(); protected override void OnDeactivated() => updateState(); @@ -104,7 +124,7 @@ namespace osu.Game.Overlays.Changelog bool textHighlighted = IsHovered; bool barExpanded = IsHovered; - if (SelectedTab.Value == null) + if (SelectedItem.Value == null) { // at listing, all badges are highlighted when user is not hovering any badge. textHighlighted |= !userHoveringArea; @@ -122,19 +142,5 @@ namespace osu.Game.Overlays.Changelog expandingBar.Expanded = barExpanded; text.FadeTo(textHighlighted ? 1 : 0.5f, transition_duration, Easing.OutQuint); } - - private bool userHoveringArea; - - public bool UserHoveringArea - { - set - { - if (value == userHoveringArea) - return; - - userHoveringArea = value; - updateState(); - } - } } } From c0f7a83f6f3d4aaab9ce0d4d9ee2ae9674d238de Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 17:10:25 +0300 Subject: [PATCH 031/387] Fix featured stream item width --- osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 6a4801bc4b..b796348242 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -14,6 +14,14 @@ namespace osu.Game.Overlays.Changelog { } + protected override float GetWidth() + { + if (Value.IsFeatured) + return base.GetWidth() * 2; + + return base.GetWidth(); + } + protected override string GetMainText() => Value.DisplayName; protected override string GetAdditionalText() => Value.LatestBuild.DisplayVersion; From 160d64eecf1cff32ed662fbfcbc129fcfe2b928f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 17:37:01 +0300 Subject: [PATCH 032/387] FriendsOnlineStatusControl basic implementation --- .../TestSceneFriendsOnlineStatusControl.cs | 52 +++++++++++++++++++ .../Overlays/Home/Friends/FriendsBundle.cs | 48 +++++++++++++++++ .../Friends/FriendsOnlineStatusControl.cs | 10 ++++ .../Home/Friends/FriendsOnlineStatusItem.cs | 21 ++++++++ 4 files changed, 131 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs create mode 100644 osu.Game/Overlays/Home/Friends/FriendsBundle.cs create mode 100644 osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs create mode 100644 osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs new file mode 100644 index 0000000000..bb64593088 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -0,0 +1,52 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Game.Overlays; +using osu.Game.Overlays.Home.Friends; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneFriendsOnlineStatusControl : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(FriendsOnlineStatusControl), + typeof(FriendsOnlineStatusItem), + typeof(OverlayUpdateStreamControl<>), + typeof(OverlayUpdateStreamItem<>), + typeof(FriendsBundle) + }; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + private FriendsOnlineStatusControl control; + + [SetUp] + public void SetUp() => Schedule(() => + { + Clear(); + Add(control = new FriendsOnlineStatusControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + }); + + [Test] + public void Populate() + { + AddStep(@"Populate", () => control.Populate(new List + { + new FriendsBundle(FriendsOnlineStatus.All, 100), + new FriendsBundle(FriendsOnlineStatus.Online, 50), + new FriendsBundle(FriendsOnlineStatus.Offline, 50), + })); + } + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs new file mode 100644 index 0000000000..e0f841da9a --- /dev/null +++ b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs @@ -0,0 +1,48 @@ +// 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 osuTK.Graphics; + +namespace osu.Game.Overlays.Home.Friends +{ + public class FriendsBundle + { + public FriendsOnlineStatus Status { get; } + + public int Amount { get; } + + public Color4 Colour => getColour(); + + public FriendsBundle(FriendsOnlineStatus status, int amount) + { + Status = status; + Amount = amount; + } + + private Color4 getColour() + { + switch (Status) + { + default: + throw new ArgumentException($@"{Status} status does not provide a colour in {nameof(getColour)}."); + + case FriendsOnlineStatus.All: + return Color4.White; + + case FriendsOnlineStatus.Online: + return Color4.Lime; + + case FriendsOnlineStatus.Offline: + return Color4.Black; + } + } + } + + public enum FriendsOnlineStatus + { + All, + Online, + Offline + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs new file mode 100644 index 0000000000..abcd04bb0e --- /dev/null +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs @@ -0,0 +1,10 @@ +// 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.Overlays.Home.Friends +{ + public class FriendsOnlineStatusControl : OverlayUpdateStreamControl + { + protected override OverlayUpdateStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs new file mode 100644 index 0000000000..0c77ef20b9 --- /dev/null +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK.Graphics; + +namespace osu.Game.Overlays.Home.Friends +{ + public class FriendsOnlineStatusItem : OverlayUpdateStreamItem + { + public FriendsOnlineStatusItem(FriendsBundle value) + : base(value) + { + } + + protected override string GetMainText() => Value.Status.ToString(); + + protected override string GetAdditionalText() => Value.Amount.ToString(); + + protected override Color4 GetBarColour() => Value.Colour; + } +} From 83dad93b6d499555d8c015049599dc3df153dff6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 18:08:51 +0300 Subject: [PATCH 033/387] Make Populate() accept list of users --- .../TestSceneFriendsOnlineStatusControl.cs | 23 +++++++++++++++---- .../Friends/FriendsOnlineStatusControl.cs | 16 +++++++++++++ osu.Game/Overlays/OverlayUpdateStreamItem.cs | 2 +- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index bb64593088..87e7d848a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Home.Friends; +using osu.Game.Users; namespace osu.Game.Tests.Visual.UserInterface { @@ -41,12 +43,25 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void Populate() { - AddStep(@"Populate", () => control.Populate(new List + AddStep("Populate", () => control.Populate(new List { - new FriendsBundle(FriendsOnlineStatus.All, 100), - new FriendsBundle(FriendsOnlineStatus.Online, 50), - new FriendsBundle(FriendsOnlineStatus.Offline, 50), + new User + { + IsOnline = true + }, + new User + { + IsOnline = false + }, + new User + { + IsOnline = false + } })); + + AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Amount == 3); + AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Amount == 1); + AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Amount == 2); } } } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs index abcd04bb0e..a92de9dbeb 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs @@ -1,10 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Game.Users; + namespace osu.Game.Overlays.Home.Friends { public class FriendsOnlineStatusControl : OverlayUpdateStreamControl { protected override OverlayUpdateStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); + + public void Populate(List users) + { + var userCount = users.Count; + var onlineUsersCount = users.Count(user => user.IsOnline); + + AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount)); + + Current.Value = Items.FirstOrDefault(); + } } } diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index 5014aac5b0..bf8e6ac3b9 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -40,7 +40,7 @@ namespace osu.Game.Overlays private FillFlowContainer text; private ExpandingBar expandingBar; - public OverlayUpdateStreamItem(T value) + protected OverlayUpdateStreamItem(T value) : base(value) { } From 06b23b626ecaab8754c602087e9af752979e51f2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:15:10 +0300 Subject: [PATCH 034/387] Simplify test scene setup --- .../TestSceneFriendsOnlineStatusControl.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 87e7d848a8..614e99ee0e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -30,14 +30,10 @@ namespace osu.Game.Tests.Visual.UserInterface private FriendsOnlineStatusControl control; [SetUp] - public void SetUp() => Schedule(() => + public void SetUp() => Schedule(() => Child = control = new FriendsOnlineStatusControl { - Clear(); - Add(control = new FriendsOnlineStatusControl - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }); [Test] From c22f61b2b1ad9ec6663727b79eddb3616d9799f3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:28:47 +0300 Subject: [PATCH 035/387] Move colour selection to the FriendsOnlineStatusItem --- .../Changelog/ChangelogUpdateStreamItem.cs | 3 ++- .../Overlays/Home/Friends/FriendsBundle.cs | 23 ------------------- .../Home/Friends/FriendsOnlineStatusItem.cs | 20 +++++++++++++++- osu.Game/Overlays/OverlayUpdateStreamItem.cs | 6 ++--- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index b796348242..189c156b35 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using Humanizer; +using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osuTK.Graphics; @@ -28,6 +29,6 @@ namespace osu.Game.Overlays.Changelog protected override string GetInfoText() => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; - protected override Color4 GetBarColour() => Value.Colour; + protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } } diff --git a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs index e0f841da9a..aa403feffc 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs @@ -1,9 +1,6 @@ // 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 osuTK.Graphics; - namespace osu.Game.Overlays.Home.Friends { public class FriendsBundle @@ -12,31 +9,11 @@ namespace osu.Game.Overlays.Home.Friends public int Amount { get; } - public Color4 Colour => getColour(); - public FriendsBundle(FriendsOnlineStatus status, int amount) { Status = status; Amount = amount; } - - private Color4 getColour() - { - switch (Status) - { - default: - throw new ArgumentException($@"{Status} status does not provide a colour in {nameof(getColour)}."); - - case FriendsOnlineStatus.All: - return Color4.White; - - case FriendsOnlineStatus.Online: - return Color4.Lime; - - case FriendsOnlineStatus.Offline: - return Color4.Black; - } - } } public enum FriendsOnlineStatus diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 0c77ef20b9..bd480aebe8 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -1,6 +1,8 @@ // 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 osu.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Overlays.Home.Friends @@ -16,6 +18,22 @@ namespace osu.Game.Overlays.Home.Friends protected override string GetAdditionalText() => Value.Amount.ToString(); - protected override Color4 GetBarColour() => Value.Colour; + protected override Color4 GetBarColour(OsuColour colours) + { + switch (Value.Status) + { + default: + throw new ArgumentException($@"{Value.Status} status does not provide a colour in {nameof(GetBarColour)}."); + + case FriendsOnlineStatus.All: + return Color4.White; + + case FriendsOnlineStatus.Online: + return colours.GreenLight; + + case FriendsOnlineStatus.Offline: + return Color4.Black; + } + } } } diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index bf8e6ac3b9..ce9aca6f1f 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { Size = new Vector2(GetWidth(), 60); Padding = new MarginPadding(5); @@ -81,7 +81,7 @@ namespace osu.Game.Overlays expandingBar = new ExpandingBar { Anchor = Anchor.TopCentre, - Colour = GetBarColour(), + Colour = GetBarColour(colours), ExpandedSize = 4, CollapsedSize = 2, Expanded = true @@ -98,7 +98,7 @@ namespace osu.Game.Overlays protected virtual string GetInfoText() => string.Empty; - protected abstract Color4 GetBarColour(); + protected abstract Color4 GetBarColour(OsuColour colours); protected virtual float GetWidth() => tab_width; From 4d5445b5dc63d8b2c3ff889c70cd5f573b37d25d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:31:06 +0300 Subject: [PATCH 036/387] Rename Amount to Count --- .../UserInterface/TestSceneFriendsOnlineStatusControl.cs | 6 +++--- osu.Game/Overlays/Home/Friends/FriendsBundle.cs | 6 +++--- osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 614e99ee0e..45f8a029a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -55,9 +55,9 @@ namespace osu.Game.Tests.Visual.UserInterface } })); - AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Amount == 3); - AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Amount == 1); - AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Amount == 2); + AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Count == 3); + AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Count == 1); + AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Count == 2); } } } diff --git a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs index aa403feffc..75d00dfef8 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs @@ -7,12 +7,12 @@ namespace osu.Game.Overlays.Home.Friends { public FriendsOnlineStatus Status { get; } - public int Amount { get; } + public int Count { get; } - public FriendsBundle(FriendsOnlineStatus status, int amount) + public FriendsBundle(FriendsOnlineStatus status, int count) { Status = status; - Amount = amount; + Count = count; } } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index bd480aebe8..4043c72bc4 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Home.Friends protected override string GetMainText() => Value.Status.ToString(); - protected override string GetAdditionalText() => Value.Amount.ToString(); + protected override string GetAdditionalText() => Value.Count.ToString(); protected override Color4 GetBarColour(OsuColour colours) { From 17f2baf600fd703a1f6769bbc08c9298019eb07a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:35:32 +0300 Subject: [PATCH 037/387] Remove GetWidth function --- .../Changelog/ChangelogUpdateStreamItem.cs | 10 ++-------- osu.Game/Overlays/OverlayUpdateStreamItem.cs | 14 ++++---------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 189c156b35..5a16fc8ddf 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -13,14 +13,8 @@ namespace osu.Game.Overlays.Changelog public ChangelogUpdateStreamItem(APIUpdateStream stream) : base(stream) { - } - - protected override float GetWidth() - { - if (Value.IsFeatured) - return base.GetWidth() * 2; - - return base.GetWidth(); + if (stream.IsFeatured) + Width *= 2; } protected override string GetMainText() => Value.DisplayName; diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index ce9aca6f1f..0cdf894f73 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -11,16 +11,12 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Allocation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics; -using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays { public abstract class OverlayUpdateStreamItem : TabItem { - private const float transition_duration = 100; - private const float tab_width = 100; - public readonly Bindable SelectedItem = new Bindable(); private bool userHoveringArea; @@ -43,14 +39,14 @@ namespace osu.Game.Overlays protected OverlayUpdateStreamItem(T value) : base(value) { + Height = 60; + Width = 100; + Padding = new MarginPadding(5); } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Size = new Vector2(GetWidth(), 60); - Padding = new MarginPadding(5); - AddRange(new Drawable[] { text = new FillFlowContainer @@ -100,8 +96,6 @@ namespace osu.Game.Overlays protected abstract Color4 GetBarColour(OsuColour colours); - protected virtual float GetWidth() => tab_width; - protected override void OnActivated() => updateState(); protected override void OnDeactivated() => updateState(); @@ -140,7 +134,7 @@ namespace osu.Game.Overlays } expandingBar.Expanded = barExpanded; - text.FadeTo(textHighlighted ? 1 : 0.5f, transition_duration, Easing.OutQuint); + text.FadeTo(textHighlighted ? 1 : 0.5f, 100, Easing.OutQuint); } } } From 6fca3e5a468d09d065a975e5d5eaab567ee6e5f7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:39:12 +0300 Subject: [PATCH 038/387] Remove functions with get-only properties --- .../Overlays/Changelog/ChangelogUpdateStreamItem.cs | 6 +++--- .../Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 4 ++-- osu.Game/Overlays/OverlayUpdateStreamItem.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 5a16fc8ddf..84b33fcde9 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -17,11 +17,11 @@ namespace osu.Game.Overlays.Changelog Width *= 2; } - protected override string GetMainText() => Value.DisplayName; + protected override string GetMainText => Value.DisplayName; - protected override string GetAdditionalText() => Value.LatestBuild.DisplayVersion; + protected override string GetAdditionalText => Value.LatestBuild.DisplayVersion; - protected override string GetInfoText() => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + protected override string GetInfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 4043c72bc4..1025fc8146 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Home.Friends { } - protected override string GetMainText() => Value.Status.ToString(); + protected override string GetMainText => Value.Status.ToString(); - protected override string GetAdditionalText() => Value.Count.ToString(); + protected override string GetAdditionalText => Value.Count.ToString(); protected override Color4 GetBarColour(OsuColour colours) { diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index 0cdf894f73..459daeb3a5 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -58,17 +58,17 @@ namespace osu.Game.Overlays { new OsuSpriteText { - Text = GetMainText(), + Text = GetMainText, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), }, new OsuSpriteText { - Text = GetAdditionalText(), + Text = GetAdditionalText, Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), }, new OsuSpriteText { - Text = GetInfoText(), + Text = GetInfoText, Font = OsuFont.GetFont(size: 10), Colour = colourProvider.Foreground1 }, @@ -88,11 +88,11 @@ namespace osu.Game.Overlays SelectedItem.BindValueChanged(_ => updateState(), true); } - protected abstract string GetMainText(); + protected abstract string GetMainText { get; } - protected abstract string GetAdditionalText(); + protected abstract string GetAdditionalText { get; } - protected virtual string GetInfoText() => string.Empty; + protected virtual string GetInfoText => string.Empty; protected abstract Color4 GetBarColour(OsuColour colours); From e2ed13b39248d10f52ffae58f4e51ed594a8951e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:40:10 +0300 Subject: [PATCH 039/387] Trim whitespace --- osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 84b33fcde9..79824db572 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Changelog : base(stream) { if (stream.IsFeatured) - Width *= 2; + Width *= 2; } protected override string GetMainText => Value.DisplayName; From 5e218697c5a525d8289cfa3d9959b1e0df6ac2d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 4 Mar 2020 09:46:53 +0900 Subject: [PATCH 040/387] Use stacked positions --- .../TestSceneFollowPoints.cs | 46 +++++++++++++++++++ .../Connections/FollowPointConnection.cs | 4 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index 94ca2d4cd1..87da7ef417 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; @@ -114,6 +117,22 @@ namespace osu.Game.Rulesets.Osu.Tests assertGroups(); } + [Test] + public void TestStackedObjects() + { + addObjectsStep(() => new OsuHitObject[] + { + new HitCircle { Position = new Vector2(300, 100) }, + new HitCircle + { + Position = new Vector2(300, 300), + StackHeight = 20 + }, + }); + + assertDirections(); + } + private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) }, @@ -207,6 +226,33 @@ namespace osu.Game.Rulesets.Osu.Tests }); } + private void assertDirections() + { + AddAssert("group directions are correct", () => + { + for (int i = 0; i < hitObjectContainer.Count; i++) + { + DrawableOsuHitObject expectedStart = getObject(i); + DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; + + if (expectedEnd == null) + continue; + + var points = getGroup(i).ChildrenOfType().ToArray(); + if (points.Length == 0) + continue; + + float expectedDirection = MathF.Atan2(expectedStart.Position.Y - expectedEnd.Position.Y, expectedStart.Position.X - expectedEnd.Position.X); + float realDirection = MathF.Atan2(expectedStart.Position.Y - points[^1].Position.Y, expectedStart.Position.X - points[^1].Position.X); + + if (!Precision.AlmostEquals(expectedDirection, realDirection)) + throw new AssertionException($"Expected group {i} in direction {expectedDirection}, but was {realDirection}."); + } + + return true; + }); + } + private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index]; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 921b23cb13..3e9c0f341b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -104,8 +104,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections return; } - Vector2 startPosition = osuStart.EndPosition; - Vector2 endPosition = osuEnd.Position; + Vector2 startPosition = osuStart.StackedEndPosition; + Vector2 endPosition = osuEnd.StackedPosition; double endTime = osuEnd.StartTime; Vector2 distanceVector = endPosition - startPosition; From c3f840cc1a5071ee92c758315d5c41ee12cd1dec Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Tue, 3 Mar 2020 17:12:01 -0800 Subject: [PATCH 041/387] Fix Autoplay = false and AllowFail behavior --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 6 +++--- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 13 ++++++++----- osu.Game/Tests/Visual/PlayerTestScene.cs | 2 ++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 1fc9ccccd1..20cb9ef05d 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -42,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); - protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); + protected override TestPlayer CreateReplayPlayer(Score score, bool allowFail) => new ScoreAccessibleTestPlayer(score, allowFail); private class ScoreAccessibleTestPlayer : TestPlayer { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - public ScoreAccessibleTestPlayer(Score score) - : base(score) + public ScoreAccessibleTestPlayer(Score score, bool allowFail) + : base(score, allowFail) { } } diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index 11612d0eca..8a9cdf009b 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Game.Beatmaps; -using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -56,22 +55,26 @@ namespace osu.Game.Tests.Visual var score = currentTest.Autoplay ? ruleset.GetAutoplayMod().CreateReplayScore(Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value)) - : new Score { Replay = new Replay() }; + : null; - return CreateReplayPlayer(score); + return CreateReplayPlayer(score, AllowFail); } /// /// Creates the for a test case. /// /// The . - protected virtual TestPlayer CreateReplayPlayer(Score score) => new TestPlayer(score); + /// Whether the player can fail. + protected virtual TestPlayer CreateReplayPlayer(Score score, bool allowFail) => new TestPlayer(score, allowFail); protected class TestPlayer : TestReplayPlayer { - public TestPlayer(Score score) + protected override bool AllowFail { get; } + + public TestPlayer(Score score, bool allowFail) : base(score, false, false) { + AllowFail = allowFail; } } diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 0d5aac8cfd..17ad6e80df 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -68,6 +68,8 @@ namespace osu.Game.Tests.Visual Beatmap.Value = CreateWorkingBeatmap(beatmap); + SelectedMods.Value = Array.Empty(); + if (!AllowFail) { var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail); From f42523352735dff805a37713893107bd6ab7d2cd Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 08:41:21 +0300 Subject: [PATCH 042/387] Basic UserCard implementation --- .../Visual/UserInterface/TestSceneUserCard.cs | 35 +++++++++ .../UserInterfaceV2/Users/UserCard.cs | 73 +++++++++++++++++++ .../UserInterfaceV2/Users/UserGridCard.cs | 18 +++++ 3 files changed, 126 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs new file mode 100644 index 0000000000..96a5a1e9ca --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -0,0 +1,35 @@ +// 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 osu.Framework.Graphics; +using osu.Game.Graphics.UserInterfaceV2.Users; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneUserCard : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(UserCard), + typeof(UserGridCard), + }; + + public TestSceneUserCard() + { + Add(new UserGridCard(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs new file mode 100644 index 0000000000..a83a523f9a --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -0,0 +1,73 @@ +// 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 osu.Framework.Allocation; +using osu.Game.Overlays; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.Containers; +using osu.Game.Users; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using System.Collections.Generic; +using osu.Framework.Input.Events; + +namespace osu.Game.Graphics.UserInterfaceV2.Users +{ + public abstract class UserCard : OsuHoverContainer, IHasContextMenu + { + [Resolved(canBeNull:true)] + private UserProfileOverlay profileOverlay { get; set; } + + protected override IEnumerable EffectTargets => null; + + public User User { get; } + + public UserCard(User user) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + + User = user; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Action = () => profileOverlay?.ShowUser(User); + + Masking = true; + BorderColour = colours.GreyVioletLighter; + + Add(new DelayedLoadUnloadWrapper(() => new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User, + }, 300, 5000) + { + RelativeSizeAxes = Axes.Both, + }); + } + + protected override bool OnHover(HoverEvent e) + { + BorderThickness = 2; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + BorderThickness = 0; + base.OnHoverLost(e); + } + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action), + }; + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs new file mode 100644 index 0000000000..8a322a20f3 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2.Users +{ + public class UserGridCard : UserCard + { + public UserGridCard(User user) + : base(user) + { + Size = new Vector2(290, 120); + CornerRadius = 10; + } + } +} From 1b5222f39638ee5fe0f9033902040d4e7646f716 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 08:53:14 +0300 Subject: [PATCH 043/387] Baisc UserListCard implementation --- .../Visual/UserInterface/TestSceneUserCard.cs | 41 +++++++++++++++---- .../UserInterfaceV2/Users/UserListCard.cs | 19 +++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index 96a5a1e9ca..9639aeb49e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2.Users; using osu.Game.Users; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { @@ -15,20 +17,45 @@ namespace osu.Game.Tests.Visual.UserInterface { typeof(UserCard), typeof(UserGridCard), + typeof(UserListCard) }; public TestSceneUserCard() { - Add(new UserGridCard(new User - { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" - }) + Add(new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new UserGridCard(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new UserListCard(new User + { + Username = @"peppy", + Id = 2, + Country = new Country { FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + IsSupporter = true, + SupportLevel = 3, + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } }); } } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs new file mode 100644 index 0000000000..c270690086 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Users; +using osu.Framework.Graphics; + +namespace osu.Game.Graphics.UserInterfaceV2.Users +{ + public class UserListCard : UserCard + { + public UserListCard(User user) + : base(user) + { + RelativeSizeAxes = Axes.X; + Height = 40; + CornerRadius = 6; + } + } +} From b7d34b399d9f6819c3e71d9b20a3718495e89cc1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 09:10:51 +0300 Subject: [PATCH 044/387] Adjust background presentation for UserListCard --- .../Visual/UserInterface/TestSceneUserCard.cs | 5 +++ .../UserInterfaceV2/Users/UserCard.cs | 37 +++++++++++++------ .../UserInterfaceV2/Users/UserListCard.cs | 11 ++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index 9639aeb49e..db934fc822 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2.Users; +using osu.Game.Overlays; using osu.Game.Users; using osuTK; @@ -20,6 +22,9 @@ namespace osu.Game.Tests.Visual.UserInterface typeof(UserListCard) }; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + public TestSceneUserCard() { Add(new FillFlowContainer diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs index a83a523f9a..9a89c0118b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -13,17 +13,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System.Collections.Generic; using osu.Framework.Input.Events; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Graphics.UserInterfaceV2.Users { public abstract class UserCard : OsuHoverContainer, IHasContextMenu { - [Resolved(canBeNull:true)] - private UserProfileOverlay profileOverlay { get; set; } + public User User { get; } protected override IEnumerable EffectTargets => null; - public User User { get; } + protected DelayedLoadUnloadWrapper Background; public UserCard(User user) { @@ -33,23 +33,36 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users User = user; } + [Resolved(canBeNull: true)] + private UserProfileOverlay profileOverlay { get; set; } + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { Action = () => profileOverlay?.ShowUser(User); Masking = true; BorderColour = colours.GreyVioletLighter; - Add(new DelayedLoadUnloadWrapper(() => new UserCoverBackground + AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - User = User, - }, 300, 5000) - { - RelativeSizeAxes = Axes.Both, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5 + }, + Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User, + }, 300, 5000) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + } }); } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs index c270690086..e445404d40 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs @@ -3,6 +3,10 @@ using osu.Game.Users; using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Colour; +using osu.Framework.Extensions.Color4Extensions; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2.Users { @@ -15,5 +19,12 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users Height = 40; CornerRadius = 6; } + + [BackgroundDependencyLoader] + private void load() + { + Background.Width = 0.5f; + Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White); + } } } From 416b9e4e6f38d64113ed35654bf6435be44c0cda Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 14:28:18 +0800 Subject: [PATCH 045/387] fix beatmap status display --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index 446a075ae4..e80034f494 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -59,7 +59,25 @@ namespace osu.Game.Overlays.BeatmapSet if (online.Ranked.HasValue) { - fields.Add(new Field("ranked", online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); + string verb = "ranked"; + + switch (online.Status) + { + case BeatmapSetOnlineStatus.Ranked: + verb = "ranked"; + break; + case BeatmapSetOnlineStatus.Approved: + verb = "approved"; + break; + case BeatmapSetOnlineStatus.Qualified: + verb = "qualified"; + break; + case BeatmapSetOnlineStatus.Loved: + verb = "loved"; + break; + } + + fields.Add(new Field(verb, online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); } else if (online.LastUpdated.HasValue) { From fbd0dfd71b6c889794197e6e747ef9a99a185273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9CNate?= Date: Wed, 4 Mar 2020 14:55:51 +0800 Subject: [PATCH 046/387] add blank lines --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index e80034f494..f3bf0b8e5e 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -66,12 +66,15 @@ namespace osu.Game.Overlays.BeatmapSet case BeatmapSetOnlineStatus.Ranked: verb = "ranked"; break; + case BeatmapSetOnlineStatus.Approved: verb = "approved"; break; + case BeatmapSetOnlineStatus.Qualified: verb = "qualified"; break; + case BeatmapSetOnlineStatus.Loved: verb = "loved"; break; From 6ea3af1951562c7f0720745abb7441b755412dc0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 10:35:43 +0300 Subject: [PATCH 047/387] Implement layout for UserListCard --- .../Visual/UserInterface/TestSceneUserCard.cs | 2 + .../UserInterfaceV2/Users/UserCard.cs | 76 ++++++++++++++++- .../UserInterfaceV2/Users/UserGridCard.cs | 7 ++ .../UserInterfaceV2/Users/UserListCard.cs | 81 ++++++++++++++++++- 4 files changed, 163 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index db934fc822..8134af8208 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -55,6 +55,8 @@ namespace osu.Game.Tests.Visual.UserInterface CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", IsSupporter = true, SupportLevel = 3, + IsOnline = false, + LastVisit = DateTimeOffset.Now }) { Anchor = Anchor.Centre, diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs index 9a89c0118b..916c0c2401 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -14,6 +14,13 @@ using osu.Framework.Graphics.Containers; using System.Collections.Generic; using osu.Framework.Input.Events; using osu.Framework.Graphics.Shapes; +using JetBrains.Annotations; +using osu.Game.Users.Drawables; +using osuTK; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Framework.Graphics.Sprites; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2.Users { @@ -36,8 +43,11 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users [Resolved(canBeNull: true)] private UserProfileOverlay profileOverlay { get; set; } + [Resolved] + private OsuColour colours { get; set; } + [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { Action = () => profileOverlay?.ShowUser(User); @@ -62,10 +72,72 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, - } + }, + CreateLayout() }); } + [NotNull] + protected abstract Drawable CreateLayout(); + + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar + { + User = User, + Masking = true, + OpenOnClick = { Value = false } + }; + + protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) + { + Size = new Vector2(39, 26) + }; + + protected OsuSpriteText CreateUsername() => new OsuSpriteText + { + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), + Text = User.Username, + }; + + protected SpriteIcon CreateStatusIcon() => new SpriteIcon + { + Icon = FontAwesome.Regular.Circle, + Size = new Vector2(25), + Colour = User.IsOnline ? colours.GreenLight : Color4.Black + }; + + protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) + { + var status = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical + }; + + var alignment = rightAlignedChildren ? Anchor.x2 : Anchor.x0; + + if (!User.IsOnline && User.LastVisit.HasValue) + { + status.Add(new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => + { + text.Anchor = Anchor.y1 | alignment; + text.Origin = Anchor.y1 | alignment; + text.AutoSizeAxes = Axes.Both; + text.AddText(@"Last seen "); + text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)); + })); + } + + status.Add(new OsuSpriteText + { + Anchor = Anchor.y1 | alignment, + Origin = Anchor.y1 | alignment, + Font = OsuFont.GetFont(size: 17), + Text = User.IsOnline ? @"Online" : @"Offline" + }); + + return status; + } + protected override bool OnHover(HoverEvent e) { BorderThickness = 2; diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs index 8a322a20f3..7390d33b4e 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs @@ -1,6 +1,8 @@ // 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.Game.Users; using osuTK; @@ -14,5 +16,10 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users Size = new Vector2(290, 120); CornerRadius = 10; } + + protected override Drawable CreateLayout() => new Container + { + RelativeSizeAxes = Axes.Both, + }; } } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs index e445404d40..9be6ba9c65 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs @@ -7,6 +7,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Colour; using osu.Framework.Extensions.Color4Extensions; using osuTK.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; +using osu.Game.Overlays.Profile.Header.Components; namespace osu.Game.Graphics.UserInterfaceV2.Users { @@ -24,7 +27,83 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users private void load() { Background.Width = 0.5f; - Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White); + Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(User.IsOnline ? 0.6f : 0.7f)); + } + + protected override Drawable CreateLayout() + { + FillFlowContainer details; + + var layout = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + details = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + CreateAvatar().With(avatar => + { + avatar.Anchor = Anchor.CentreLeft; + avatar.Origin = Anchor.CentreLeft; + avatar.Size = new Vector2(40); + avatar.Masking = false; + }), + CreateFlag().With(flag => + { + flag.Anchor = Anchor.CentreLeft; + flag.Origin = Anchor.CentreLeft; + }), + CreateUsername().With(username => + { + username.Anchor = Anchor.CentreLeft; + username.Origin = Anchor.CentreLeft; + }) + } + }, + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Margin = new MarginPadding { Right = 10 }, + Children = new Drawable[] + { + CreateStatusIcon().With(icon => + { + icon.Anchor = Anchor.CentreRight; + icon.Origin = Anchor.CentreRight; + }), + CreateStatusMessage(true).With(message => + { + message.Anchor = Anchor.CentreRight; + message.Origin = Anchor.CentreRight; + }) + } + } + } + }; + + if (User.IsSupporter) + { + details.Add(new SupporterIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 20, + SupportLevel = User.SupportLevel + }); + } + + return layout; } } } From 7464a486d980a4180aa86f3404a77e8234688bcd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 16:35:45 +0900 Subject: [PATCH 048/387] Fix DummyWorkingBeatmap's track completion attempting to change game-wide beatmap --- osu.Game/OsuGame.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5781a7fbc4..69d5a9a583 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -428,11 +428,17 @@ namespace osu.Game } } - private void currentTrackCompleted() => Schedule(() => + private void currentTrackCompleted() { - if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled) - musicController.NextTrack(); - }); + if (Beatmap.Value is DummyWorkingBeatmap) + return; + + Schedule(() => + { + if (Beatmap.Value.Track.Looping && !Beatmap.Disabled) + musicController.NextTrack(); + }); + } #endregion From eaa77bce142cb905b8be09fdf5ecf2e5bd2e8242 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 16:43:35 +0800 Subject: [PATCH 049/387] Use ToString().ToLowerInvariant() * https://github.com/ppy/osu/pull/8128#issuecomment-594360083 --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 23 +--------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index f3bf0b8e5e..31c1439c8f 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -59,28 +59,7 @@ namespace osu.Game.Overlays.BeatmapSet if (online.Ranked.HasValue) { - string verb = "ranked"; - - switch (online.Status) - { - case BeatmapSetOnlineStatus.Ranked: - verb = "ranked"; - break; - - case BeatmapSetOnlineStatus.Approved: - verb = "approved"; - break; - - case BeatmapSetOnlineStatus.Qualified: - verb = "qualified"; - break; - - case BeatmapSetOnlineStatus.Loved: - verb = "loved"; - break; - } - - fields.Add(new Field(verb, online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); + fields.Add(new Field(online.Status.ToString().ToLowerInvariant(), online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); } else if (online.LastUpdated.HasValue) { From 184d10a75a9f0c4cdc839d8b62d1540ac2650781 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 4 Mar 2020 15:45:22 +0700 Subject: [PATCH 050/387] Revert "Reduce social overlay/direct overlay paddings" This reverts commit cb1129218135bd2b1e2d3a287d381c13e08725d6. --- osu.Game/Overlays/SearchableList/SearchableListOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index 72796df6d5..0783c64c20 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.SearchableList { public abstract class SearchableListOverlay : FullscreenOverlay { - public const float WIDTH_PADDING = 10; + public const float WIDTH_PADDING = 80; protected SearchableListOverlay(OverlayColourScheme colourScheme) : base(colourScheme) From a1dc59500699d1d83875c58a48bf9b0e489ec187 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 4 Mar 2020 15:46:35 +0700 Subject: [PATCH 051/387] Change scroll container padding --- osu.Game/Overlays/SearchableList/SearchableListOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index 0783c64c20..d6174e0733 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.SearchableList { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WIDTH_PADDING, Bottom = 50 }, + Padding = new MarginPadding { Horizontal = 10, Bottom = 50 }, Direction = FillDirection.Vertical, }, }, From 15e47d843295e1d22d9f13b5e72c546b362caed7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 12:20:49 +0300 Subject: [PATCH 052/387] Implement layout for UserGridCard --- .../Visual/UserInterface/TestSceneUserCard.cs | 47 ++++++--- .../UserInterfaceV2/Users/UserCard.cs | 1 - .../UserInterfaceV2/Users/UserGridCard.cs | 95 ++++++++++++++++++- .../UserInterfaceV2/Users/UserListCard.cs | 1 - 4 files changed, 126 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index 8134af8208..2966ee242a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -36,16 +36,33 @@ namespace osu.Game.Tests.Visual.UserInterface Spacing = new Vector2(0, 10), Children = new Drawable[] { - new UserGridCard(new User + new FillFlowContainer { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" - }) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new UserGridCard(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + IsOnline = true, + IsSupporter = true, + SupportLevel = 3, + }), + new UserGridCard(new User + { + Username = @"Evast", + Id = 8195163, + Country = new Country { FlagName = @"BY" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsOnline = false, + LastVisit = DateTimeOffset.Now + }) + } }, new UserListCard(new User { @@ -57,11 +74,15 @@ namespace osu.Game.Tests.Visual.UserInterface SupportLevel = 3, IsOnline = false, LastVisit = DateTimeOffset.Now - }) + }), + new UserListCard(new User { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Username = @"chocomint", + Id = 124493, + Country = new Country { FlagName = @"KR" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", + IsOnline = true, + }), } }); } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs index 916c0c2401..dc26518692 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -83,7 +83,6 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar { User = User, - Masking = true, OpenOnClick = { Value = false } }; diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs index 7390d33b4e..b6e704e901 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs @@ -1,8 +1,10 @@ // 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.Game.Overlays.Profile.Header.Components; using osu.Game.Users; using osuTK; @@ -10,6 +12,8 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users { public class UserGridCard : UserCard { + private const int margin = 10; + public UserGridCard(User user) : base(user) { @@ -17,9 +21,94 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users CornerRadius = 10; } - protected override Drawable CreateLayout() => new Container + [BackgroundDependencyLoader] + private void load() { - RelativeSizeAxes = Axes.Both, - }; + Background.FadeTo(User.IsOnline ? 0.6f : 0.7f); + } + + protected override Drawable CreateLayout() + { + FillFlowContainer details; + + var layout = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(margin), + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + CreateAvatar().With(avatar => + { + avatar.Size = new Vector2(60); + avatar.Margin = new MarginPadding { Bottom = margin }; + avatar.Masking = true; + avatar.CornerRadius = 6; + }), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 7), + Margin = new MarginPadding { Left = margin }, + Children = new Drawable[] + { + details = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6), + Children = new Drawable[] + { + CreateFlag(), + } + }, + CreateUsername(), + } + } + }, + new Drawable[] + { + CreateStatusIcon().With(icon => + { + icon.Anchor = Anchor.Centre; + icon.Origin = Anchor.Centre; + }), + CreateStatusMessage(false).With(message => + { + message.Anchor = Anchor.CentreLeft; + message.Origin = Anchor.CentreLeft; + message.Margin = new MarginPadding { Left = margin }; + }) + } + } + } + }; + + if (User.IsSupporter) + { + details.Add(new SupporterIcon + { + Height = 26, + SupportLevel = User.SupportLevel + }); + } + + return layout; + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs index 9be6ba9c65..685556035e 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs @@ -53,7 +53,6 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users avatar.Anchor = Anchor.CentreLeft; avatar.Origin = Anchor.CentreLeft; avatar.Size = new Vector2(40); - avatar.Masking = false; }), CreateFlag().With(flag => { From 8a437e1b5452bb14f5be2f1c66c20dd1af616467 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 12:42:21 +0300 Subject: [PATCH 053/387] Add ability to send pm via context menu --- .../Visual/UserInterface/TestSceneUserCard.cs | 107 +++++++++--------- .../UserInterfaceV2/Users/UserCard.cs | 17 ++- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index 2966ee242a..0d5967b5a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2.Users; using osu.Game.Overlays; using osu.Game.Users; @@ -27,62 +28,66 @@ namespace osu.Game.Tests.Visual.UserInterface public TestSceneUserCard() { - Add(new FillFlowContainer + Add(new OsuContextMenuContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(0, 10), - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - new FillFlowContainer + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(0, 10), + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - Children = new Drawable[] + new FillFlowContainer { - new UserGridCard(new User + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Children = new Drawable[] { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", - IsOnline = true, - IsSupporter = true, - SupportLevel = 3, - }), - new UserGridCard(new User - { - Username = @"Evast", - Id = 8195163, - Country = new Country { FlagName = @"BY" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, - LastVisit = DateTimeOffset.Now - }) - } - }, - new UserListCard(new User - { - Username = @"peppy", - Id = 2, - Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - IsSupporter = true, - SupportLevel = 3, - IsOnline = false, - LastVisit = DateTimeOffset.Now - }), - new UserListCard(new User - { - Username = @"chocomint", - Id = 124493, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", - IsOnline = true, - }), + new UserGridCard(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + IsOnline = true, + IsSupporter = true, + SupportLevel = 3, + }), + new UserGridCard(new User + { + Username = @"Evast", + Id = 8195163, + Country = new Country { FlagName = @"BY" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsOnline = false, + LastVisit = DateTimeOffset.Now + }) + } + }, + new UserListCard(new User + { + Username = @"peppy", + Id = 2, + Country = new Country { FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + IsSupporter = true, + SupportLevel = 3, + IsOnline = false, + LastVisit = DateTimeOffset.Now + }), + new UserListCard(new User + { + Username = @"chocomint", + Id = 124493, + Country = new Country { FlagName = @"KR" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", + IsOnline = true, + }), + } } }); } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs index dc26518692..4808990c70 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -18,9 +18,9 @@ using JetBrains.Annotations; using osu.Game.Users.Drawables; using osuTK; using osu.Game.Graphics.Sprites; -using osu.Game.Overlays.Profile.Header.Components; using osu.Framework.Graphics.Sprites; using osuTK.Graphics; +using osu.Game.Online.Chat; namespace osu.Game.Graphics.UserInterfaceV2.Users { @@ -32,7 +32,7 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users protected DelayedLoadUnloadWrapper Background; - public UserCard(User user) + protected UserCard(User user) { if (user == null) throw new ArgumentNullException(nameof(user)); @@ -43,6 +43,12 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users [Resolved(canBeNull: true)] private UserProfileOverlay profileOverlay { get; set; } + [Resolved(canBeNull: true)] + private ChannelManager channelManager { get; set; } + + [Resolved(canBeNull: true)] + private ChatOverlay chatOverlay { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -54,7 +60,7 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users Masking = true; BorderColour = colours.GreyVioletLighter; - AddRange(new Drawable[] + AddRange(new[] { new Box { @@ -152,6 +158,11 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users public MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action), + new OsuMenuItem("Send message", MenuItemType.Standard, () => + { + channelManager?.OpenPrivateChannel(User); + chatOverlay?.Show(); + }) }; } } From bac35b2a68f15a0abe079f94a17883e6c040ef68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 19:09:52 +0900 Subject: [PATCH 054/387] Handle beatmap track changing in a saner way --- osu.Game/OsuGame.cs | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 69d5a9a583..0be9e6cdaa 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -396,18 +396,27 @@ namespace osu.Game private void beatmapChanged(ValueChangedEvent beatmap) { - var nextBeatmap = beatmap.NewValue; - if (nextBeatmap?.Track != null) - nextBeatmap.Track.Completed += currentTrackCompleted; - - var oldBeatmap = beatmap.OldValue; - if (oldBeatmap?.Track != null) - oldBeatmap.Track.Completed -= currentTrackCompleted; + beatmap.OldValue?.CancelAsyncLoad(); updateModDefaults(); - oldBeatmap?.CancelAsyncLoad(); - nextBeatmap?.BeginAsyncLoad(); + var newBeatmap = beatmap.NewValue; + + if (newBeatmap != null) + { + newBeatmap.Track.Completed += () => Scheduler.AddOnce(() => trackCompleted(newBeatmap)); + newBeatmap.BeginAsyncLoad(); + } + + void trackCompleted(WorkingBeatmap b) + { + // the source of track completion is the audio thread, so the beatmap may have changed before a firing. + if (Beatmap.Value != b) + return; + + if (Beatmap.Value.Track.Looping && !Beatmap.Disabled) + musicController.NextTrack(); + } } private void modsChanged(ValueChangedEvent> mods) @@ -428,18 +437,6 @@ namespace osu.Game } } - private void currentTrackCompleted() - { - if (Beatmap.Value is DummyWorkingBeatmap) - return; - - Schedule(() => - { - if (Beatmap.Value.Track.Looping && !Beatmap.Disabled) - musicController.NextTrack(); - }); - } - #endregion private ScheduledDelegate performFromMainMenuTask; From 38d91ccd0d20dcd4f9f4e016f03da00da2ba8152 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 19:07:34 +0900 Subject: [PATCH 055/387] Add comment regarding no-longer-required schedule --- osu.Game/OsuGameBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a890331f05..67aa4a8d4d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -211,6 +211,10 @@ namespace osu.Game Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); Beatmap = new NonNullableBindable(defaultBeatmap); + + // ScheduleAfterChildren is safety against something in the current frame accessing the previous beatmap's track + // and potentially causing a reload of it after just unloading. + // Note that the reason for this being added *has* been resolved, so it may be feasible to remover this if required. Beatmap.BindValueChanged(b => ScheduleAfterChildren(() => { // compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) From a62550b3239791678ab70613ad8ac522101e5788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 20:14:18 +0900 Subject: [PATCH 056/387] Reapply filters on next change after a forced beatmap display --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 7 +++++++ osu.Game/Screens/Select/Carousel/CarouselItem.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 2 ++ osu.Game/Screens/Select/SongSelect.cs | 7 +++++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 2ffb73f226..d78d407748 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -25,6 +25,13 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); + if (Beatmap.Equals(criteria.SelectedBeatmap)) + { + // bypass filtering for selected beatmap + Filtered.Value = false; + return; + } + bool match = criteria.Ruleset == null || Beatmap.RulesetID == criteria.Ruleset.ID || diff --git a/osu.Game/Screens/Select/Carousel/CarouselItem.cs b/osu.Game/Screens/Select/Carousel/CarouselItem.cs index 1108b72bd2..79c1a4cb6b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselItem.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select.Carousel /// /// This item is not in a hidden state. /// - public bool Visible => State.Value == CarouselItemState.Selected || (State.Value != CarouselItemState.Collapsed && !Filtered.Value); + public bool Visible => State.Value != CarouselItemState.Collapsed && !Filtered.Value; public virtual List Drawables { diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 9fa57af01d..a5c4e2961d 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -15,6 +15,8 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; + public BeatmapInfo SelectedBeatmap; + public OptionalRange StarDifficulty; public OptionalRange ApproachRate; public OptionalRange DrainRate; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 67626d1e4f..90ad7a7fdd 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -392,8 +392,11 @@ namespace osu.Game.Screens.Select } // Even if a ruleset mismatch was not the cause (ie. a text filter is applied), - // we still want to forcefully show the new beatmap, bypassing filters. - Carousel.SelectBeatmap(e.NewValue.BeatmapInfo); + // we still want to temporarily show the new beatmap, bypassing filters. + // This will be undone the next time the user changes the filter. + var criteria = FilterControl.CreateCriteria(); + criteria.SelectedBeatmap = e.NewValue.BeatmapInfo; + Carousel.Filter(criteria); } } From 6631b074421aabf4e50f5a061c10475864a8244a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 14:58:15 +0300 Subject: [PATCH 057/387] Refactor to replace existing panels --- .../Online/TestSceneAccountCreationOverlay.cs | 2 +- .../Visual/Online/TestSceneSocialOverlay.cs | 5 +- .../Visual/Online/TestSceneUserPanel.cs | 25 +- .../Visual/UserInterface/TestSceneUserCard.cs | 95 ------ .../Screens/Editors/TeamEditorScreen.cs | 2 +- .../UserInterfaceV2/Users/UserCard.cs | 168 ---------- .../Sections/General/LoginSettings.cs | 2 +- osu.Game/Overlays/Social/SocialGridPanel.cs | 16 - osu.Game/Overlays/Social/SocialListPanel.cs | 17 - osu.Game/Overlays/Social/SocialPanel.cs | 61 ---- osu.Game/Overlays/SocialOverlay.cs | 13 +- .../UserGridPanel.cs} | 11 +- .../UserListPanel.cs} | 9 +- osu.Game/Users/UserPanel.cs | 305 ++++++++---------- osu.Game/Users/UserStatus.cs | 4 +- 15 files changed, 175 insertions(+), 560 deletions(-) delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs delete mode 100644 osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs delete mode 100644 osu.Game/Overlays/Social/SocialGridPanel.cs delete mode 100644 osu.Game/Overlays/Social/SocialListPanel.cs delete mode 100644 osu.Game/Overlays/Social/SocialPanel.cs rename osu.Game/{Graphics/UserInterfaceV2/Users/UserGridCard.cs => Users/UserGridPanel.cs} (93%) rename osu.Game/{Graphics/UserInterfaceV2/Users/UserListCard.cs => Users/UserListPanel.cs} (94%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index 31eab7f74e..a53a818065 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.Online API.Logout(); localUser = API.LocalUser.GetBoundCopy(); - localUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true); + localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true); AddStep("logout", API.Logout); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs index dbd7544b38..24341cbd05 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs @@ -18,10 +18,9 @@ namespace osu.Game.Tests.Visual.Online public override IReadOnlyList RequiredTypes => new[] { typeof(UserPanel), - typeof(SocialPanel), typeof(FilterControl), - typeof(SocialGridPanel), - typeof(SocialListPanel) + typeof(UserGridPanel), + typeof(UserListPanel) }; public TestSceneSocialOverlay() diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 54f06d6ad2..f4c96ac251 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -1,6 +1,8 @@ // 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.Bindables; using osu.Framework.Graphics; @@ -13,6 +15,13 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserPanel : OsuTestScene { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(UserPanel), + typeof(UserListPanel), + typeof(UserGridPanel), + }; + private readonly UserPanel peppy; public TestSceneUserPanel() @@ -23,18 +32,19 @@ namespace osu.Game.Tests.Visual.Online { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, Spacing = new Vector2(10f), Children = new[] { - flyte = new UserPanel(new User + flyte = new UserGridPanel(new User { Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }) { Width = 300 }, - peppy = new UserPanel(new User + peppy = new UserGridPanel(new User { Username = @"peppy", Id = 2, @@ -43,6 +53,15 @@ namespace osu.Game.Tests.Visual.Online IsSupporter = true, SupportLevel = 3, }) { Width = 300 }, + new UserListPanel(new User + { + Username = @"Evast", + Id = 8195163, + Country = new Country { FlagName = @"BY" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsOnline = false, + LastVisit = DateTimeOffset.Now + }) }, }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs deleted file mode 100644 index 0d5967b5a8..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ /dev/null @@ -1,95 +0,0 @@ -// 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 osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Cursor; -using osu.Game.Graphics.UserInterfaceV2.Users; -using osu.Game.Overlays; -using osu.Game.Users; -using osuTK; - -namespace osu.Game.Tests.Visual.UserInterface -{ - public class TestSceneUserCard : OsuTestScene - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(UserCard), - typeof(UserGridCard), - typeof(UserListCard) - }; - - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - - public TestSceneUserCard() - { - Add(new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - Children = new Drawable[] - { - new UserGridCard(new User - { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", - IsOnline = true, - IsSupporter = true, - SupportLevel = 3, - }), - new UserGridCard(new User - { - Username = @"Evast", - Id = 8195163, - Country = new Country { FlagName = @"BY" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, - LastVisit = DateTimeOffset.Now - }) - } - }, - new UserListCard(new User - { - Username = @"peppy", - Id = 2, - Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - IsSupporter = true, - SupportLevel = 3, - IsOnline = false, - LastVisit = DateTimeOffset.Now - }), - new UserListCard(new User - { - Username = @"chocomint", - Id = 124493, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", - IsOnline = true, - }), - } - } - }); - } - } -} diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index ca8bce1cca..bbbc948811 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -311,7 +311,7 @@ namespace osu.Game.Tournament.Screens.Editors private void updatePanel() { - drawableContainer.Child = new UserPanel(user) { Width = 300 }; + drawableContainer.Child = new UserGridPanel(user) { Width = 300 }; } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs deleted file mode 100644 index 4808990c70..0000000000 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ /dev/null @@ -1,168 +0,0 @@ -// 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 osu.Framework.Allocation; -using osu.Game.Overlays; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; -using osu.Framework.Graphics.Cursor; -using osu.Game.Graphics.Containers; -using osu.Game.Users; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using System.Collections.Generic; -using osu.Framework.Input.Events; -using osu.Framework.Graphics.Shapes; -using JetBrains.Annotations; -using osu.Game.Users.Drawables; -using osuTK; -using osu.Game.Graphics.Sprites; -using osu.Framework.Graphics.Sprites; -using osuTK.Graphics; -using osu.Game.Online.Chat; - -namespace osu.Game.Graphics.UserInterfaceV2.Users -{ - public abstract class UserCard : OsuHoverContainer, IHasContextMenu - { - public User User { get; } - - protected override IEnumerable EffectTargets => null; - - protected DelayedLoadUnloadWrapper Background; - - protected UserCard(User user) - { - if (user == null) - throw new ArgumentNullException(nameof(user)); - - User = user; - } - - [Resolved(canBeNull: true)] - private UserProfileOverlay profileOverlay { get; set; } - - [Resolved(canBeNull: true)] - private ChannelManager channelManager { get; set; } - - [Resolved(canBeNull: true)] - private ChatOverlay chatOverlay { get; set; } - - [Resolved] - private OsuColour colours { get; set; } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Action = () => profileOverlay?.ShowUser(User); - - Masking = true; - BorderColour = colours.GreyVioletLighter; - - AddRange(new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5 - }, - Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - User = User, - }, 300, 5000) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, - }, - CreateLayout() - }); - } - - [NotNull] - protected abstract Drawable CreateLayout(); - - protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar - { - User = User, - OpenOnClick = { Value = false } - }; - - protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) - { - Size = new Vector2(39, 26) - }; - - protected OsuSpriteText CreateUsername() => new OsuSpriteText - { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), - Text = User.Username, - }; - - protected SpriteIcon CreateStatusIcon() => new SpriteIcon - { - Icon = FontAwesome.Regular.Circle, - Size = new Vector2(25), - Colour = User.IsOnline ? colours.GreenLight : Color4.Black - }; - - protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) - { - var status = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical - }; - - var alignment = rightAlignedChildren ? Anchor.x2 : Anchor.x0; - - if (!User.IsOnline && User.LastVisit.HasValue) - { - status.Add(new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => - { - text.Anchor = Anchor.y1 | alignment; - text.Origin = Anchor.y1 | alignment; - text.AutoSizeAxes = Axes.Both; - text.AddText(@"Last seen "); - text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)); - })); - } - - status.Add(new OsuSpriteText - { - Anchor = Anchor.y1 | alignment, - Origin = Anchor.y1 | alignment, - Font = OsuFont.GetFont(size: 17), - Text = User.IsOnline ? @"Online" : @"Offline" - }); - - return status; - } - - protected override bool OnHover(HoverEvent e) - { - BorderThickness = 2; - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - BorderThickness = 0; - base.OnHoverLost(e); - } - - public MenuItem[] ContextMenuItems => new MenuItem[] - { - new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action), - new OsuMenuItem("Send message", MenuItemType.Standard, () => - { - channelManager?.OpenPrivateChannel(User); - chatOverlay?.Show(); - }) - }; - } -} diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index bf0e073350..3ab64786a2 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Settings.Sections.General }, }, }, - panel = new UserPanel(api.LocalUser.Value) + panel = new UserGridPanel(api.LocalUser.Value) { RelativeSizeAxes = Axes.X, Action = RequestHide diff --git a/osu.Game/Overlays/Social/SocialGridPanel.cs b/osu.Game/Overlays/Social/SocialGridPanel.cs deleted file mode 100644 index 6f707d640b..0000000000 --- a/osu.Game/Overlays/Social/SocialGridPanel.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Users; - -namespace osu.Game.Overlays.Social -{ - public class SocialGridPanel : SocialPanel - { - public SocialGridPanel(User user) - : base(user) - { - Width = 300; - } - } -} diff --git a/osu.Game/Overlays/Social/SocialListPanel.cs b/osu.Game/Overlays/Social/SocialListPanel.cs deleted file mode 100644 index 1ba91e9204..0000000000 --- a/osu.Game/Overlays/Social/SocialListPanel.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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.Game.Users; - -namespace osu.Game.Overlays.Social -{ - public class SocialListPanel : SocialPanel - { - public SocialListPanel(User user) - : base(user) - { - RelativeSizeAxes = Axes.X; - } - } -} diff --git a/osu.Game/Overlays/Social/SocialPanel.cs b/osu.Game/Overlays/Social/SocialPanel.cs deleted file mode 100644 index 555527670a..0000000000 --- a/osu.Game/Overlays/Social/SocialPanel.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; -using osuTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Effects; -using osu.Framework.Input.Events; -using osu.Game.Users; - -namespace osu.Game.Overlays.Social -{ - public class SocialPanel : UserPanel - { - private const double hover_transition_time = 400; - - public SocialPanel(User user) - : base(user) - { - } - - private readonly EdgeEffectParameters edgeEffectNormal = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 1f), - Radius = 2f, - Colour = Color4.Black.Opacity(0.25f), - }; - - private readonly EdgeEffectParameters edgeEffectHovered = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 5f), - Radius = 10f, - Colour = Color4.Black.Opacity(0.3f), - }; - - protected override bool OnHover(HoverEvent e) - { - Content.TweenEdgeEffectTo(edgeEffectHovered, hover_transition_time, Easing.OutQuint); - Content.MoveToY(-4, hover_transition_time, Easing.OutQuint); - - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - Content.TweenEdgeEffectTo(edgeEffectNormal, hover_transition_time, Easing.OutQuint); - Content.MoveToY(0, hover_transition_time, Easing.OutQuint); - - base.OnHoverLost(e); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - this.FadeInFromZero(200, Easing.Out); - } - } -} diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 54c978738d..5a9dd54d53 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays public class SocialOverlay : SearchableListOverlay { private readonly LoadingSpinner loading; - private FillFlowContainer panels; + private FillFlowContainer panels; protected override Color4 BackgroundColour => OsuColour.FromHex(@"60284b"); protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"672b51"); @@ -158,7 +158,7 @@ namespace osu.Game.Overlays if (Filter.DisplayStyleControl.Dropdown.Current.Value == SortDirection.Descending) sortedUsers = sortedUsers.Reverse(); - var newPanels = new FillFlowContainer + var newPanels = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -166,20 +166,21 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Top = 10 }, ChildrenEnumerable = sortedUsers.Select(u => { - SocialPanel panel; + UserPanel panel; switch (Filter.DisplayStyleControl.DisplayStyle.Value) { case PanelDisplayStyle.Grid: - panel = new SocialGridPanel(u) + panel = new UserGridPanel(u) { Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre + Origin = Anchor.TopCentre, + Width = 290, }; break; default: - panel = new SocialListPanel(u); + panel = new UserListPanel(u); break; } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs b/osu.Game/Users/UserGridPanel.cs similarity index 93% rename from osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs rename to osu.Game/Users/UserGridPanel.cs index b6e704e901..f9c5c2b0cc 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -5,26 +5,25 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Profile.Header.Components; -using osu.Game.Users; using osuTK; -namespace osu.Game.Graphics.UserInterfaceV2.Users +namespace osu.Game.Users { - public class UserGridCard : UserCard + public class UserGridPanel : UserPanel { private const int margin = 10; - public UserGridCard(User user) + public UserGridPanel(User user) : base(user) { - Size = new Vector2(290, 120); + Height = 120; CornerRadius = 10; } [BackgroundDependencyLoader] private void load() { - Background.FadeTo(User.IsOnline ? 0.6f : 0.7f); + Background.FadeTo(0.6f); } protected override Drawable CreateLayout() diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Users/UserListPanel.cs similarity index 94% rename from osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs rename to osu.Game/Users/UserListPanel.cs index 685556035e..087de13706 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Users; using osu.Framework.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics.Colour; @@ -11,11 +10,11 @@ using osu.Framework.Graphics.Containers; using osuTK; using osu.Game.Overlays.Profile.Header.Components; -namespace osu.Game.Graphics.UserInterfaceV2.Users +namespace osu.Game.Users { - public class UserListCard : UserCard + public class UserListPanel : UserPanel { - public UserListCard(User user) + public UserListPanel(User user) : base(user) { RelativeSizeAxes = Axes.X; @@ -27,7 +26,7 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users private void load() { Background.Width = 0.5f; - Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(User.IsOnline ? 0.6f : 0.7f)); + Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.6f)); } protected override Drawable CreateLayout() diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 6f34466e94..5a5f18dcfe 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -3,10 +3,8 @@ using System; using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -16,32 +14,18 @@ using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users.Drawables; +using JetBrains.Annotations; +using osu.Framework.Input.Events; namespace osu.Game.Users { - public class UserPanel : OsuClickableContainer, IHasContextMenu + public abstract class UserPanel : OsuClickableContainer, IHasContextMenu { - private const float height = 100; - private const float content_padding = 10; - private const float status_height = 30; - public readonly User User; - [Resolved(canBeNull: true)] - private OsuColour colours { get; set; } - - private Container statusBar; - private Box statusBg; - private OsuSpriteText statusMessage; - - private Container content; - protected override Container Content => content; - public readonly Bindable Status = new Bindable(); public readonly IBindable Activity = new Bindable(); @@ -50,164 +34,63 @@ namespace osu.Game.Users protected Action ViewProfile; - public UserPanel(User user) + protected DelayedLoadUnloadWrapper Background; + + private SpriteIcon statusIcon; + private OsuSpriteText statusMessage; + private TextFlowContainer lastVisitMessage; + + protected UserPanel(User user) { if (user == null) throw new ArgumentNullException(nameof(user)); User = user; - - Height = height - status_height; } - [BackgroundDependencyLoader(permitNulls: true)] - private void load(UserProfileOverlay profile) + [Resolved(canBeNull: true)] + private UserProfileOverlay profileOverlay { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() { - if (colours == null) - throw new InvalidOperationException($"{nameof(colours)} not initialized!"); + Action = () => profileOverlay?.ShowUser(User); - FillFlowContainer infoContainer; + Masking = true; + BorderColour = colours.GreyVioletLighter; - AddInternal(content = new Container + AddRange(new[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 5, - EdgeEffect = new EdgeEffectParameters + new Box { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1 }, - - Children = new Drawable[] + Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground { - new DelayedLoadUnloadWrapper(() => new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - User = User, - }, 300, 5000) - { - RelativeSizeAxes = Axes.Both, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Top = content_padding, Horizontal = content_padding }, - Children = new Drawable[] - { - new UpdateableAvatar - { - Size = new Vector2(height - status_height - content_padding * 2), - User = User, - Masking = true, - CornerRadius = 5, - OpenOnClick = { Value = false }, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, - }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = height - status_height - content_padding }, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = User.Username, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18, italics: true), - }, - infoContainer = new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.X, - Height = 20f, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5f, 0f), - Children = new Drawable[] - { - new UpdateableFlag(User.Country) - { - Width = 30f, - RelativeSizeAxes = Axes.Y, - }, - }, - }, - }, - }, - }, - }, - statusBar = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Alpha = 0f, - Children = new Drawable[] - { - statusBg = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.5f, - }, - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5f, 0f), - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Icon = FontAwesome.Regular.Circle, - Shadow = true, - Size = new Vector2(14), - }, - statusMessage = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - }, - }, - }, - }, - }, - } + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User, + }, 300, 5000) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + }, + CreateLayout() }); - if (User.IsSupporter) - { - infoContainer.Add(new SupporterIcon - { - Height = 20f, - SupportLevel = User.SupportLevel - }); - } - Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value); Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue); base.Action = ViewProfile = () => { Action?.Invoke(); - profile?.ShowUser(User); + profileOverlay?.ShowUser(User); }; } @@ -217,33 +100,105 @@ namespace osu.Game.Users Status.TriggerChange(); } + protected override bool OnHover(HoverEvent e) + { + BorderThickness = 2; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + BorderThickness = 0; + base.OnHoverLost(e); + } + + [NotNull] + protected abstract Drawable CreateLayout(); + + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar + { + User = User, + OpenOnClick = { Value = false } + }; + + protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) + { + Size = new Vector2(39, 26) + }; + + protected OsuSpriteText CreateUsername() => new OsuSpriteText + { + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), + Text = User.Username, + }; + + protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon + { + Icon = FontAwesome.Regular.Circle, + Size = new Vector2(25) + }; + + protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) + { + var statusContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical + }; + + var alignment = rightAlignedChildren ? Anchor.x2 : Anchor.x0; + + statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => + { + text.Anchor = Anchor.y1 | alignment; + text.Origin = Anchor.y1 | alignment; + text.AutoSizeAxes = Axes.Both; + text.Alpha = 0; + + if (User.LastVisit.HasValue) + { + text.AddText(@"Last seen "); + text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)); + } + })); + + statusContainer.Add(statusMessage = new OsuSpriteText + { + Anchor = Anchor.y1 | alignment, + Origin = Anchor.y1 | alignment, + Font = OsuFont.GetFont(size: 17) + }); + + return statusContainer; + } + private void displayStatus(UserStatus status, UserActivity activity = null) { - const float transition_duration = 500; + if (status != null) + { + if (activity != null && !(status is UserStatusOffline)) + { + statusMessage.Text = activity.Status; + statusIcon.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint); + } + else + { + lastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); + statusMessage.Text = status.Message; + statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); + } - if (status == null) - { - statusBar.ResizeHeightTo(0f, transition_duration, Easing.OutQuint); - statusBar.FadeOut(transition_duration, Easing.OutQuint); - this.ResizeHeightTo(height - status_height, transition_duration, Easing.OutQuint); - } - else - { - statusBar.ResizeHeightTo(status_height, transition_duration, Easing.OutQuint); - statusBar.FadeIn(transition_duration, Easing.OutQuint); - this.ResizeHeightTo(height, transition_duration, Easing.OutQuint); + return; } - if (status is UserStatusOnline && activity != null) + // Set local status according to web if it's null + if (User.IsOnline) { - statusMessage.Text = activity.Status; - statusBg.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint); - } - else - { - statusMessage.Text = status?.Message; - statusBg.FadeColour(status?.GetAppropriateColour(colours) ?? colours.Gray5, 500, Easing.OutQuint); + Status.Value = new UserStatusOnline(); + return; } + + Status.Value = new UserStatusOffline(); } public MenuItem[] ContextMenuItems => new MenuItem[] diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs index cf372560af..21c18413f4 100644 --- a/osu.Game/Users/UserStatus.cs +++ b/osu.Game/Users/UserStatus.cs @@ -15,7 +15,7 @@ namespace osu.Game.Users public class UserStatusOnline : UserStatus { public override string Message => @"Online"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.BlueDarker; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; } public abstract class UserStatusBusy : UserStatusOnline @@ -26,7 +26,7 @@ namespace osu.Game.Users public class UserStatusOffline : UserStatus { public override string Message => @"Offline"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.Gray7; + public override Color4 GetAppropriateColour(OsuColour colours) => Color4.Black; } public class UserStatusDoNotDisturb : UserStatus From 0b98bcc856582671025a333601c907198c0672b0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 15:22:08 +0300 Subject: [PATCH 058/387] Fix incorrect click action --- osu.Game/Users/UserPanel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 5a5f18dcfe..7692f70de0 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -57,8 +57,6 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load() { - Action = () => profileOverlay?.ShowUser(User); - Masking = true; BorderColour = colours.GreyVioletLighter; From 11ffe6a072e935cbcfeaef5546754c0359770470 Mon Sep 17 00:00:00 2001 From: McEndu Date: Wed, 4 Mar 2020 21:45:01 +0800 Subject: [PATCH 059/387] Remove LD_LIBRARY_PATH from vscode launch.json --- .vscode/launch.json | 42 +----------------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6480612b2e..4e8af405a2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,11 +11,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Debug)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -28,11 +23,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Release)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -45,11 +35,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tests (Debug)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -62,11 +47,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tests (Release)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -80,11 +60,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Debug)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -98,11 +73,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Release)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -116,11 +86,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tournament tests (Debug)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -134,11 +99,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tournament tests (Release)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -169,4 +129,4 @@ "externalConsole": false } ] -} \ No newline at end of file +} From f8776a0be4924acfc5673b9683d4ef8fab702390 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 22:59:49 +0900 Subject: [PATCH 060/387] Display all difficulties from overriding selection --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index d78d407748..c110608a0e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - if (Beatmap.Equals(criteria.SelectedBeatmap)) + if (Beatmap.BeatmapSet.Equals(criteria.SelectedBeatmapSet)) { // bypass filtering for selected beatmap Filtered.Value = false; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index a5c4e2961d..18be4fcac8 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; - public BeatmapInfo SelectedBeatmap; + public BeatmapSetInfo SelectedBeatmapSet; public OptionalRange StarDifficulty; public OptionalRange ApproachRate; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 90ad7a7fdd..6577ed8506 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -395,7 +395,7 @@ namespace osu.Game.Screens.Select // we still want to temporarily show the new beatmap, bypassing filters. // This will be undone the next time the user changes the filter. var criteria = FilterControl.CreateCriteria(); - criteria.SelectedBeatmap = e.NewValue.BeatmapInfo; + criteria.SelectedBeatmapSet = e.NewValue.BeatmapInfo.BeatmapSet; Carousel.Filter(criteria); } } From 9aacc3f5ae1b27cf4564f9f1bf53bd8af0485f27 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 4 Mar 2020 19:24:52 +0100 Subject: [PATCH 061/387] Replace Scores property with DisplayScores method Also adds null checks to prevent crashes in tests. --- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 32 ++++++++++--------- .../BeatmapSet/Scores/ScoresContainer.cs | 5 ++- .../Scores/TopScoreStatisticsSection.cs | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index af6bf8299f..4755522b9c 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -52,25 +52,27 @@ namespace osu.Game.Overlays.BeatmapSet.Scores highAccuracyColour = colours.GreenLight; } - public IReadOnlyList Scores + private bool showPerformancePoints; + + public void DisplayScores(IReadOnlyList scores, bool showPerformanceColumn) { - set - { - Content = null; - backgroundFlow.Clear(); + if (!scores.Any()) + return; - if (value?.Any() != true) - return; + showPerformancePoints = showPerformanceColumn; - for (int i = 0; i < value.Count; i++) - backgroundFlow.Add(new ScoreTableRowBackground(i, value[i], row_height)); + for (int i = 0; i < scores.Count; i++) + backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height)); - Columns = createHeaders(value.First()); - Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); - } + Columns = createHeaders(scores.FirstOrDefault()); + Content = scores.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); } - public bool IsBeatmapRanked { get; set; } + public void ClearScores() + { + Content = null; + backgroundFlow.Clear(); + } private TableColumn[] createHeaders(ScoreInfo score) { @@ -90,7 +92,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); - if (IsBeatmapRanked) + if (showPerformancePoints) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); columns.Add(new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); @@ -151,7 +153,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }); } - if (IsBeatmapRanked) + if (showPerformancePoints) { content.Add(new OsuSpriteText { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 5a931fffcb..be1f8c2111 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores if (value?.Scores.Any() != true) { - scoreTable.Scores = null; + scoreTable.ClearScores(); scoreTable.Hide(); return; } @@ -61,8 +61,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList(); var topScore = scoreInfos.First(); - scoreTable.Scores = scoreInfos; - scoreTable.IsBeatmapRanked = topScore.Beatmap.Status == BeatmapSetOnlineStatus.Ranked; + scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked); scoreTable.Show(); var userScore = value.UserScore; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 9ecc40eed2..a92346e0fe 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores totalScoreColumn.Text = $@"{value.TotalScore:N0}"; accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; - ppColumn.Alpha = value.Beatmap.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; + ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; ppColumn.Text = $@"{value.PP:N0}"; statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); From 5628c5102dc791fa44c96e5c2480dcc40c726eaf Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 4 Mar 2020 20:01:15 +0100 Subject: [PATCH 062/387] Remove old scores before adding new ones --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 4755522b9c..097ca27bf7 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -56,6 +56,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores public void DisplayScores(IReadOnlyList scores, bool showPerformanceColumn) { + ClearScores(); + if (!scores.Any()) return; From 55a0586b13903999e5fd3ec04f8622ed077446c6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 23:03:02 +0300 Subject: [PATCH 063/387] Move exception handling below all the cases --- osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 1025fc8146..37652a7ecb 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -22,9 +22,6 @@ namespace osu.Game.Overlays.Home.Friends { switch (Value.Status) { - default: - throw new ArgumentException($@"{Value.Status} status does not provide a colour in {nameof(GetBarColour)}."); - case FriendsOnlineStatus.All: return Color4.White; @@ -33,6 +30,9 @@ namespace osu.Game.Overlays.Home.Friends case FriendsOnlineStatus.Offline: return Color4.Black; + + default: + throw new ArgumentException($@"{Value.Status} status does not provide a colour in {nameof(GetBarColour)}."); } } } From 63219a2357ebe94d9b96c4a74ac17bdc85e57a42 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 23:06:16 +0300 Subject: [PATCH 064/387] Adjust properties naming --- .../Overlays/Changelog/ChangelogUpdateStreamItem.cs | 6 +++--- .../Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 4 ++-- osu.Game/Overlays/OverlayUpdateStreamItem.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 79824db572..9590aefa49 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -17,11 +17,11 @@ namespace osu.Game.Overlays.Changelog Width *= 2; } - protected override string GetMainText => Value.DisplayName; + protected override string MainText => Value.DisplayName; - protected override string GetAdditionalText => Value.LatestBuild.DisplayVersion; + protected override string AdditionalText => Value.LatestBuild.DisplayVersion; - protected override string GetInfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + protected override string InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 37652a7ecb..5dd7ca2c18 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Home.Friends { } - protected override string GetMainText => Value.Status.ToString(); + protected override string MainText => Value.Status.ToString(); - protected override string GetAdditionalText => Value.Count.ToString(); + protected override string AdditionalText => Value.Count.ToString(); protected override Color4 GetBarColour(OsuColour colours) { diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index 459daeb3a5..7bccb8da25 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -58,17 +58,17 @@ namespace osu.Game.Overlays { new OsuSpriteText { - Text = GetMainText, + Text = MainText, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), }, new OsuSpriteText { - Text = GetAdditionalText, + Text = AdditionalText, Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), }, new OsuSpriteText { - Text = GetInfoText, + Text = InfoText, Font = OsuFont.GetFont(size: 10), Colour = colourProvider.Foreground1 }, @@ -88,11 +88,11 @@ namespace osu.Game.Overlays SelectedItem.BindValueChanged(_ => updateState(), true); } - protected abstract string GetMainText { get; } + protected abstract string MainText { get; } - protected abstract string GetAdditionalText { get; } + protected abstract string AdditionalText { get; } - protected virtual string GetInfoText => string.Empty; + protected virtual string InfoText => string.Empty; protected abstract Color4 GetBarColour(OsuColour colours); From bd03dd9b707bba167a2ae8697626a3f6da50641a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 23:08:58 +0300 Subject: [PATCH 065/387] Adjust class naming --- .../TestSceneFriendsOnlineStatusControl.cs | 4 ++-- .../Overlays/Changelog/ChangelogUpdateStreamControl.cs | 4 ++-- .../Overlays/Changelog/ChangelogUpdateStreamItem.cs | 2 +- .../Home/Friends/FriendsOnlineStatusControl.cs | 4 ++-- .../Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 2 +- ...yUpdateStreamControl.cs => OverlayStreamControl.cs} | 10 +++++----- ...OverlayUpdateStreamItem.cs => OverlayStreamItem.cs} | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) rename osu.Game/Overlays/{OverlayUpdateStreamControl.cs => OverlayStreamControl.cs} (84%) rename osu.Game/Overlays/{OverlayUpdateStreamItem.cs => OverlayStreamItem.cs} (97%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 45f8a029a8..0d841dfef1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -19,8 +19,8 @@ namespace osu.Game.Tests.Visual.UserInterface { typeof(FriendsOnlineStatusControl), typeof(FriendsOnlineStatusItem), - typeof(OverlayUpdateStreamControl<>), - typeof(OverlayUpdateStreamItem<>), + typeof(OverlayStreamControl<>), + typeof(OverlayStreamItem<>), typeof(FriendsBundle) }; diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs index 555f0904d6..509a6dabae 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs @@ -5,8 +5,8 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Changelog { - public class ChangelogUpdateStreamControl : OverlayUpdateStreamControl + public class ChangelogUpdateStreamControl : OverlayStreamControl { - protected override OverlayUpdateStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); + protected override OverlayStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); } } diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 9590aefa49..f8e1ac0c84 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -8,7 +8,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Changelog { - public class ChangelogUpdateStreamItem : OverlayUpdateStreamItem + public class ChangelogUpdateStreamItem : OverlayStreamItem { public ChangelogUpdateStreamItem(APIUpdateStream stream) : base(stream) diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs index a92de9dbeb..196f01ab4a 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs @@ -7,9 +7,9 @@ using osu.Game.Users; namespace osu.Game.Overlays.Home.Friends { - public class FriendsOnlineStatusControl : OverlayUpdateStreamControl + public class FriendsOnlineStatusControl : OverlayStreamControl { - protected override OverlayUpdateStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); + protected override OverlayStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); public void Populate(List users) { diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 5dd7ca2c18..d9b780ce46 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -7,7 +7,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Home.Friends { - public class FriendsOnlineStatusItem : OverlayUpdateStreamItem + public class FriendsOnlineStatusItem : OverlayStreamItem { public FriendsOnlineStatusItem(FriendsBundle value) : base(value) diff --git a/osu.Game/Overlays/OverlayUpdateStreamControl.cs b/osu.Game/Overlays/OverlayStreamControl.cs similarity index 84% rename from osu.Game/Overlays/OverlayUpdateStreamControl.cs rename to osu.Game/Overlays/OverlayStreamControl.cs index 0fdf6c0111..8b6aca6d5d 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamControl.cs +++ b/osu.Game/Overlays/OverlayStreamControl.cs @@ -10,9 +10,9 @@ using JetBrains.Annotations; namespace osu.Game.Overlays { - public abstract class OverlayUpdateStreamControl : TabControl + public abstract class OverlayStreamControl : TabControl { - protected OverlayUpdateStreamControl() + protected OverlayStreamControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -28,7 +28,7 @@ namespace osu.Game.Overlays }); [NotNull] - protected abstract OverlayUpdateStreamItem CreateStreamItem(T value); + protected abstract OverlayStreamItem CreateStreamItem(T value); protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { @@ -39,7 +39,7 @@ namespace osu.Game.Overlays protected override bool OnHover(HoverEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.Children.OfType>()) streamBadge.UserHoveringArea = true; return base.OnHover(e); @@ -47,7 +47,7 @@ namespace osu.Game.Overlays protected override void OnHoverLost(HoverLostEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.Children.OfType>()) streamBadge.UserHoveringArea = false; base.OnHoverLost(e); diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs similarity index 97% rename from osu.Game/Overlays/OverlayUpdateStreamItem.cs rename to osu.Game/Overlays/OverlayStreamItem.cs index 7bccb8da25..630d3a0a22 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -15,7 +15,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class OverlayUpdateStreamItem : TabItem + public abstract class OverlayStreamItem : TabItem { public readonly Bindable SelectedItem = new Bindable(); @@ -36,7 +36,7 @@ namespace osu.Game.Overlays private FillFlowContainer text; private ExpandingBar expandingBar; - protected OverlayUpdateStreamItem(T value) + protected OverlayStreamItem(T value) : base(value) { Height = 60; From 997be65be2329d3a3376f1ca16914622e767da0f Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 4 Mar 2020 21:19:26 +0100 Subject: [PATCH 066/387] Improve colouring logic --- .../Historical/DrawableMostPlayedBeatmap.cs | 17 +++++++++----- .../Profile/Sections/ProfileItemContainer.cs | 22 ++++++++++++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index dc8492562a..5b7c5efbe2 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -37,10 +37,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { - ProfileItemContainer container; - AddRangeInternal(new Drawable[] { new UpdateableBeatmapSetCover @@ -63,7 +61,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical CornerRadius = corner_radius, Children = new Drawable[] { - container = new ProfileItemContainer + new MostPlayedBeatmapContainer { Child = new Container { @@ -108,9 +106,16 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } }); + } - container.IdleColour = colourProvider.Background4; - container.HoverColour = colourProvider.Background3; + private class MostPlayedBeatmapContainer : ProfileItemContainer + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = colourProvider.Background4; + HoverColour = colourProvider.Background3; + } } private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer diff --git a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs index a48f21f52b..1ab9ed3f82 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs @@ -21,13 +21,29 @@ namespace osu.Game.Overlays.Profile.Sections private Color4 idleColour; - public Color4 IdleColour + protected Color4 IdleColour { get => idleColour; - set => idleColour = background.Colour = value; + set + { + idleColour = value; + if (!IsHovered) + background.Colour = value; + } } - public Color4 HoverColour { get; set; } + private Color4 hoverColour; + + protected Color4 HoverColour + { + get => hoverColour; + set + { + hoverColour = value; + if (IsHovered) + background.Colour = value; + } + } public ProfileItemContainer() { From e3e66991b08c4f26b0b8f46a4ef8c414f775a6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:02:36 +0100 Subject: [PATCH 067/387] Move initialisation logic to [SetUp] --- .../Visual/Online/TestSceneUserPanel.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 54f06d6ad2..597ca00fb8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -13,13 +13,16 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserPanel : OsuTestScene { - private readonly UserPanel peppy; + private readonly Bindable activity = new Bindable(); - public TestSceneUserPanel() + private UserPanel peppy; + + [SetUp] + public void SetUp() => Schedule(() => { UserPanel flyte; - Add(new FillFlowContainer + Child = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -44,11 +47,12 @@ namespace osu.Game.Tests.Visual.Online SupportLevel = 3, }) { Width = 300 }, }, - }); + }; flyte.Status.Value = new UserStatusOnline(); peppy.Status.Value = null; - } + peppy.Activity.BindTo(activity); + }); [Test] public void UserStatusesTests() @@ -62,10 +66,6 @@ namespace osu.Game.Tests.Visual.Online [Test] public void UserActivitiesTests() { - Bindable activity = new Bindable(); - - peppy.Activity.BindTo(activity); - AddStep("idle", () => { activity.Value = null; }); AddStep("spectating", () => { activity.Value = new UserActivity.Spectating(); }); AddStep("solo", () => { activity.Value = new UserActivity.SoloGame(null, null); }); From 5b25b5dfabf972d05f4829fd55b363c575be4421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:04:49 +0100 Subject: [PATCH 068/387] Change brace style --- .../Visual/Online/TestSceneUserPanel.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 597ca00fb8..55017db479 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -57,21 +57,21 @@ namespace osu.Game.Tests.Visual.Online [Test] public void UserStatusesTests() { - AddStep("online", () => { peppy.Status.Value = new UserStatusOnline(); }); - AddStep(@"do not disturb", () => { peppy.Status.Value = new UserStatusDoNotDisturb(); }); - AddStep(@"offline", () => { peppy.Status.Value = new UserStatusOffline(); }); - AddStep(@"null status", () => { peppy.Status.Value = null; }); + AddStep("online", () => peppy.Status.Value = new UserStatusOnline()); + AddStep(@"do not disturb", () => peppy.Status.Value = new UserStatusDoNotDisturb()); + AddStep(@"offline", () => peppy.Status.Value = new UserStatusOffline()); + AddStep(@"null status", () => peppy.Status.Value = null); } [Test] public void UserActivitiesTests() { - AddStep("idle", () => { activity.Value = null; }); - AddStep("spectating", () => { activity.Value = new UserActivity.Spectating(); }); - AddStep("solo", () => { activity.Value = new UserActivity.SoloGame(null, null); }); - AddStep("choosing", () => { activity.Value = new UserActivity.ChoosingBeatmap(); }); - AddStep("editing", () => { activity.Value = new UserActivity.Editing(null); }); - AddStep("modding", () => { activity.Value = new UserActivity.Modding(); }); + AddStep("idle", () => activity.Value = null); + AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); + AddStep("solo", () => activity.Value = new UserActivity.SoloGame(null, null)); + AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); + AddStep("editing", () => activity.Value = new UserActivity.Editing(null)); + AddStep("modding", () => activity.Value = new UserActivity.Modding()); } } } From 1bd49d50c771846168209054257ae132e854e8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:05:08 +0100 Subject: [PATCH 069/387] Remove unnecessary raw string prefixes --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 55017db479..f128584b1a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -58,9 +58,9 @@ namespace osu.Game.Tests.Visual.Online public void UserStatusesTests() { AddStep("online", () => peppy.Status.Value = new UserStatusOnline()); - AddStep(@"do not disturb", () => peppy.Status.Value = new UserStatusDoNotDisturb()); - AddStep(@"offline", () => peppy.Status.Value = new UserStatusOffline()); - AddStep(@"null status", () => peppy.Status.Value = null); + AddStep("do not disturb", () => peppy.Status.Value = new UserStatusDoNotDisturb()); + AddStep("offline", () => peppy.Status.Value = new UserStatusOffline()); + AddStep("null status", () => peppy.Status.Value = null); } [Test] From 5fa2638e81619fe0ba9fa726561972ea9886ac1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:05:48 +0100 Subject: [PATCH 070/387] Rename tests to adhere to convention --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index f128584b1a..ad5dfb371a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Online }); [Test] - public void UserStatusesTests() + public void TestUserStatus() { AddStep("online", () => peppy.Status.Value = new UserStatusOnline()); AddStep("do not disturb", () => peppy.Status.Value = new UserStatusDoNotDisturb()); @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void UserActivitiesTests() + public void TestUserActivity() { AddStep("idle", () => activity.Value = null); AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); From afa3ce494da1856810c549bf84efa262f59d259f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:07:02 +0100 Subject: [PATCH 071/387] Set online status in activity test The test would check nothing otherwise. --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index ad5dfb371a..fae3e9b17b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -66,6 +66,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestUserActivity() { + AddStep("set online status", () => peppy.Status.Value = new UserStatusOnline()); + AddStep("idle", () => activity.Value = null); AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); AddStep("solo", () => activity.Value = new UserActivity.SoloGame(null, null)); From b8889318dbcc6d7b6c8c28c08d4b063b7918f6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:13:31 +0100 Subject: [PATCH 072/387] Pass rulesets to solo game status --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index fae3e9b17b..80fcef2ed2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets; using osu.Game.Users; using osuTK; @@ -17,6 +19,9 @@ namespace osu.Game.Tests.Visual.Online private UserPanel peppy; + [Resolved] + private RulesetStore rulesetStore { get; set; } + [SetUp] public void SetUp() => Schedule(() => { @@ -70,10 +75,15 @@ namespace osu.Game.Tests.Visual.Online AddStep("idle", () => activity.Value = null); AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); - AddStep("solo", () => activity.Value = new UserActivity.SoloGame(null, null)); + AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0)); + AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1)); + AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); + AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); AddStep("editing", () => activity.Value = new UserActivity.Editing(null)); AddStep("modding", () => activity.Value = new UserActivity.Modding()); } + + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.SoloGame(null, rulesetStore.GetRuleset(rulesetId)); } } From d297bf69578cc58b276c89e213943f00d00e90b3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 01:41:55 +0300 Subject: [PATCH 073/387] Adjust background presentation --- osu.Game/Users/UserGridPanel.cs | 2 +- osu.Game/Users/UserListPanel.cs | 2 +- osu.Game/Users/UserPanel.cs | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index f9c5c2b0cc..4bd40b3e76 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -23,7 +23,7 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load() { - Background.FadeTo(0.6f); + Background.FadeTo(0.4f); } protected override Drawable CreateLayout() diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index 087de13706..1c3ae20577 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -26,7 +26,7 @@ namespace osu.Game.Users private void load() { Background.Width = 0.5f; - Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.6f)); + Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.3f)); } protected override Drawable CreateLayout() diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 7692f70de0..2606d669b7 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -51,6 +51,9 @@ namespace osu.Game.Users [Resolved(canBeNull: true)] private UserProfileOverlay profileOverlay { get; set; } + [Resolved(canBeNull: true)] + private OverlayColourProvider colourProvider { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -58,14 +61,14 @@ namespace osu.Game.Users private void load() { Masking = true; - BorderColour = colours.GreyVioletLighter; + BorderColour = colourProvider?.Light1 ?? colours.GreyVioletLighter; AddRange(new[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Gray1 + Colour = colourProvider?.Background4 ?? colours.Gray1 }, Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground { From 06b2c70a04a76239127642573878d49322082ad1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 01:45:49 +0300 Subject: [PATCH 074/387] Simplify alignment setting for status message --- osu.Game/Users/UserPanel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 2606d669b7..265406c6d3 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -68,7 +68,7 @@ namespace osu.Game.Users new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider?.Background4 ?? colours.Gray1 + Colour = colourProvider?.Background5 ?? colours.Gray1 }, Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground { @@ -147,12 +147,12 @@ namespace osu.Game.Users Direction = FillDirection.Vertical }; - var alignment = rightAlignedChildren ? Anchor.x2 : Anchor.x0; + var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft; statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => { - text.Anchor = Anchor.y1 | alignment; - text.Origin = Anchor.y1 | alignment; + text.Anchor = alignment; + text.Origin = alignment; text.AutoSizeAxes = Axes.Both; text.Alpha = 0; @@ -165,8 +165,8 @@ namespace osu.Game.Users statusContainer.Add(statusMessage = new OsuSpriteText { - Anchor = Anchor.y1 | alignment, - Origin = Anchor.y1 | alignment, + Anchor = alignment, + Origin = alignment, Font = OsuFont.GetFont(size: 17) }); From 22d43c26aace948380c694a68ce640202aca0425 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 02:00:46 +0300 Subject: [PATCH 075/387] Adjust some text values --- osu.Game/Users/UserPanel.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 265406c6d3..c06306c7b0 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -129,7 +129,7 @@ namespace osu.Game.Users protected OsuSpriteText CreateUsername() => new OsuSpriteText { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), + Font = OsuFont.GetFont(size: 19, weight: FontWeight.Bold, italics: true), Text = User.Username, }; @@ -149,7 +149,7 @@ namespace osu.Game.Users var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft; - statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => + statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold)).With(text => { text.Anchor = alignment; text.Origin = alignment; @@ -159,7 +159,10 @@ namespace osu.Game.Users if (User.LastVisit.HasValue) { text.AddText(@"Last seen "); - text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)); + text.AddText(new DrawableDate(User.LastVisit.Value, italic: false) + { + Shadow = false + }); } })); @@ -167,7 +170,7 @@ namespace osu.Game.Users { Anchor = alignment, Origin = alignment, - Font = OsuFont.GetFont(size: 17) + Font = OsuFont.GetFont(size: 17, weight: FontWeight.SemiBold) }); return statusContainer; From 2e996eeb7e6f36082fad19f37dd655d1825a1007 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 02:10:38 +0300 Subject: [PATCH 076/387] Refactor displayStatus function and add comments --- osu.Game/Users/UserPanel.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index c06306c7b0..9d577bcc2a 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -180,22 +180,23 @@ namespace osu.Game.Users { if (status != null) { + // Set status message based on activity (if we have one) and status is not offline if (activity != null && !(status is UserStatusOffline)) { statusMessage.Text = activity.Status; statusIcon.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint); + return; } - else - { - lastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); - statusMessage.Text = status.Message; - statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); - } + + // Otherwise use only status + lastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); + statusMessage.Text = status.Message; + statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); return; } - // Set local status according to web if it's null + // Fallback to web status if local one is null if (User.IsOnline) { Status.Value = new UserStatusOnline(); From 13752ffe9d86836affa22126bdf381e3b5cb9d33 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 02:31:19 +0300 Subject: [PATCH 077/387] Revisit UserGridPanel layout --- osu.Game/Users/UserGridPanel.cs | 58 ++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index 4bd40b3e76..b0ce557a19 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -45,6 +45,7 @@ namespace osu.Game.Users RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, margin), new Dimension() }, Content = new[] @@ -54,32 +55,57 @@ namespace osu.Game.Users CreateAvatar().With(avatar => { avatar.Size = new Vector2(60); - avatar.Margin = new MarginPadding { Bottom = margin }; avatar.Masking = true; avatar.CornerRadius = 6; }), - new FillFlowContainer + new Container { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 7), - Margin = new MarginPadding { Left = margin }, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = margin }, + Child = new GridContainer { - details = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(6), - Children = new Drawable[] - { - CreateFlag(), - } + new Dimension() }, - CreateUsername(), + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + details = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6), + Children = new Drawable[] + { + CreateFlag(), + } + } + }, + new Drawable[] + { + CreateUsername().With(username => + { + username.Anchor = Anchor.CentreLeft; + username.Origin = Anchor.CentreLeft; + }) + } + } } } }, + new[] + { + Empty(), + Empty() + }, new Drawable[] { CreateStatusIcon().With(icon => From 6b1fdcf9a2f7a3d798f33f3870396416b838eb0a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 02:38:30 +0300 Subject: [PATCH 078/387] Minor adjustments --- osu.Game/Users/UserGridPanel.cs | 2 +- osu.Game/Users/UserPanel.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index b0ce557a19..e62a834d6d 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -23,7 +23,7 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load() { - Background.FadeTo(0.4f); + Background.FadeTo(0.3f); } protected override Drawable CreateLayout() diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 9d577bcc2a..f0a7895547 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -130,6 +130,7 @@ namespace osu.Game.Users protected OsuSpriteText CreateUsername() => new OsuSpriteText { Font = OsuFont.GetFont(size: 19, weight: FontWeight.Bold, italics: true), + Shadow = false, Text = User.Username, }; From 6b44021f4d0c61163190088ae68433d4694e3452 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 03:33:14 +0300 Subject: [PATCH 079/387] Change username text size back to 20 --- osu.Game/Users/UserPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index f0a7895547..a900a55dab 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -129,7 +129,7 @@ namespace osu.Game.Users protected OsuSpriteText CreateUsername() => new OsuSpriteText { - Font = OsuFont.GetFont(size: 19, weight: FontWeight.Bold, italics: true), + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), Shadow = false, Text = User.Username, }; From ce3786cfd912b2cff9e7431e13cb7761352de8af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:11:27 +0900 Subject: [PATCH 080/387] Rename to ModTestScene (is no longer a sandbox) --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 2 +- .../Visual/{ModSandboxTestScene.cs => ModTestScene.cs} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Tests/Visual/{ModSandboxTestScene.cs => ModTestScene.cs} (95%) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 20cb9ef05d..c2a7a5003f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -12,7 +12,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModDifficultyAdjust : ModSandboxTestScene + public class TestSceneOsuModDifficultyAdjust : ModTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(OsuModDifficultyAdjust)).ToList(); diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs similarity index 95% rename from osu.Game/Tests/Visual/ModSandboxTestScene.cs rename to osu.Game/Tests/Visual/ModTestScene.cs index 8a9cdf009b..1ff061dfac 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -13,16 +13,16 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { - public abstract class ModSandboxTestScene : PlayerTestScene + public abstract class ModTestScene : PlayerTestScene { protected sealed override bool HasCustomSteps => true; public override IReadOnlyList RequiredTypes => new[] { - typeof(ModSandboxTestScene) + typeof(ModTestScene) }; - protected ModSandboxTestScene(Ruleset ruleset) + protected ModTestScene(Ruleset ruleset) : base(ruleset) { } From 2a581ef24786102d41709f08cde6747060418ed1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:15:17 +0900 Subject: [PATCH 081/387] Remove required types --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index c2a7a5003f..4a284022e2 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -1,9 +1,6 @@ // 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 System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; @@ -14,8 +11,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModDifficultyAdjust : ModTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(OsuModDifficultyAdjust)).ToList(); - public TestSceneOsuModDifficultyAdjust() : base(new OsuRuleset()) { From 0f1f1d1a6b91ce361913fbf2aa8bb6fe8a23fa01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:18:37 +0900 Subject: [PATCH 082/387] Remove unused "name" parameter --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 6 +++--- osu.Game/Tests/Visual/ModTestScene.cs | 8 +------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 4a284022e2..8ff55c9728 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -17,21 +17,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestNoAdjustment() => CreateModTest(new ModTestCaseData("no adjustment", new OsuModDifficultyAdjust()) + public void TestNoAdjustment() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust()) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestCircleSize10() => CreateModTest(new ModTestCaseData("cs = 10", new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + public void TestCircleSize10() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestApproachRate10() => CreateModTest(new ModTestCaseData("ar = 10", new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + public void TestApproachRate10() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 1ff061dfac..04f93fc683 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -97,19 +97,13 @@ namespace osu.Game.Tests.Visual [CanBeNull] public Func PassCondition; - /// - /// The name of this test case, displayed in the test browser. - /// - public readonly string Name; - /// /// The this test case tests. /// public readonly Mod Mod; - public ModTestCaseData(string name, Mod mod) + public ModTestCaseData(Mod mod) { - Name = name; Mod = mod; } } From 3b19467eadbcdf0bec89b240a7d981a599391acd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:19:42 +0900 Subject: [PATCH 083/387] ModTestCaseData -> ModTestData --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 6 +++--- osu.Game/Tests/Visual/ModTestScene.cs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 8ff55c9728..427f25fe11 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -17,21 +17,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestNoAdjustment() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust()) + public void TestNoAdjustment() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust()) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestCircleSize10() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + public void TestCircleSize10() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestApproachRate10() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + public void TestApproachRate10() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 04f93fc683..a8b40a5a68 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -27,11 +27,11 @@ namespace osu.Game.Tests.Visual { } - private ModTestCaseData currentTest; + private ModTestData currentTest; - protected void CreateModTest(ModTestCaseData testCaseData) => CreateTest(() => + protected void CreateModTest(ModTestData testData) => CreateTest(() => { - AddStep("set test data", () => currentTest = testCaseData); + AddStep("set test data", () => currentTest = testData); }); public override void TearDownSteps() @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual } } - protected class ModTestCaseData + protected class ModTestData { /// /// Whether to use a replay to simulate an auto-play. True by default. @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual /// public readonly Mod Mod; - public ModTestCaseData(Mod mod) + public ModTestData(Mod mod) { Mod = mod; } From fadebcdc03188cb171f453153530e55c6be6c7a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:21:25 +0900 Subject: [PATCH 084/387] Move all sets to object initialiser for code formatting reasons --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 9 ++++++--- osu.Game/Tests/Visual/ModTestScene.cs | 7 +------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 427f25fe11..e4b1e30bcd 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -17,22 +17,25 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestNoAdjustment() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust()) + public void TestNoAdjustment() => CreateModTest(new ModTestData() { + Mod = new OsuModDifficultyAdjust(), Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestCircleSize10() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + public void TestCircleSize10() => CreateModTest(new ModTestData { + Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }, Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestApproachRate10() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + public void TestApproachRate10() => CreateModTest(new ModTestData { + Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }, Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index a8b40a5a68..3d12001cca 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -100,12 +100,7 @@ namespace osu.Game.Tests.Visual /// /// The this test case tests. /// - public readonly Mod Mod; - - public ModTestData(Mod mod) - { - Mod = mod; - } + public Mod Mod; } } } From 5200633f9fa19b8f6994ed6b7ef0ec51d18befdf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 11:25:07 +0900 Subject: [PATCH 085/387] Centralise TestPlayer implementations as much as possible --- .../TestSceneAutoJuiceStream.cs | 3 +- .../TestSceneOsuFlashlight.cs | 4 +- .../TestSceneSkinFallbacks.cs | 5 +- .../TestSceneSpinnerRotation.cs | 5 +- .../TestSceneSwellJudgements.cs | 27 ---------- .../TestSceneTaikoSuddenDeath.cs | 18 ++----- .../Background/TestSceneUserDimBackgrounds.cs | 10 ++-- .../Visual/Gameplay/TestSceneAutoplay.cs | 24 +++------ .../Gameplay/TestSceneGameplayRewinding.cs | 49 ++++--------------- .../Visual/Gameplay/TestScenePause.cs | 7 +-- .../Gameplay/TestScenePauseWhenInactive.cs | 5 +- .../Visual/Gameplay/TestScenePlayerLoader.cs | 13 +---- osu.Game/Tests/Visual/PlayerTestScene.cs | 5 +- osu.Game/Tests/Visual/TestPlayer.cs | 28 +++++++++++ 14 files changed, 63 insertions(+), 140 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 74a9c05bf9..ed7bfb9a44 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Screens.Play; using osu.Game.Tests.Visual; using osuTK; @@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests return beatmap; } - protected override Player CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); return base.CreatePlayer(ruleset); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs index 412effe176..19736a7709 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs @@ -3,13 +3,13 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneOsuFlashlight : TestSceneOsuPlayer { - protected override Player CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), }; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 4da1b1dae0..d39e24fc1f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -18,7 +18,6 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Visual; @@ -56,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void checkNextHitObject(string skin) => AddUntilStep($"check skin from {skin}", () => { - var firstObject = ((TestPlayer)Player).DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault(); + var firstObject = Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault(); if (firstObject == null) return false; @@ -75,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audio { get; set; } - protected override Player CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin); protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, audio, testBeatmapSkin); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 5cf571d961..ea006ec607 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Tests.Visual; using osuTK; using System.Collections.Generic; using System.Linq; @@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests base.SetUpSteps(); AddUntilStep("wait for track to start running", () => track.IsRunning); - AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)((TestPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.First()); + AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First()); } [Test] @@ -89,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep($"seek to {time}", () => track.Seek(time)); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, ((TestPlayer)Player).DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs index ccacc50de1..303f0163b1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs @@ -1,23 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { public class TestSceneSwellJudgements : PlayerTestScene { - protected new TestPlayer Player => (TestPlayer)base.Player; - public TestSceneSwellJudgements() : base(new TaikoRuleset()) { @@ -49,25 +42,5 @@ namespace osu.Game.Rulesets.Taiko.Tests return beatmap; } - - protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer(); - - protected class TestPlayer : Player - { - public readonly List Results = new List(); - - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - public TestPlayer() - : base(false, false) - { - } - - [BackgroundDependencyLoader] - private void load() - { - ScoreProcessor.NewJudgement += r => Results.Add(r); - } - } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 140433a523..2ab041e191 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -4,11 +4,9 @@ using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests @@ -22,10 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Tests protected override bool AllowFail => true; - protected override Player CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { new TaikoModSuddenDeath() }).ToArray(); - return new ScoreAccessiblePlayer(); + return base.CreatePlayer(ruleset); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => @@ -49,20 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Setup judgements", () => { judged = false; - ((ScoreAccessiblePlayer)Player).ScoreProcessor.NewJudgement += b => judged = true; + Player.ScoreProcessor.NewJudgement += b => judged = true; }); AddUntilStep("swell judged", () => judged); AddAssert("not failed", () => !Player.HasFailed); } - - private class ScoreAccessiblePlayer : TestPlayer - { - public ScoreAccessiblePlayer() - : base(false, false) - { - } - - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - } } } diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 6d014ca1ca..06a155e78b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Background private DummySongSelect songSelect; private TestPlayerLoader playerLoader; - private TestPlayer player; + private LoadBlockingTestPlayer player; private BeatmapManager manager; private RulesetStore rulesets; @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Background public void PlayerLoaderSettingsHoverTest() { setupUserSettings(); - AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new TestPlayer { BlockLoad = true }))); + AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true }))); AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false); AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent()); AddStep("Trigger background preview", () => @@ -268,7 +268,7 @@ namespace osu.Game.Tests.Visual.Background { setupUserSettings(); - AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new TestPlayer(allowPause)))); + AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer(allowPause)))); AddUntilStep("Wait for Player Loader to load", () => playerLoader.IsLoaded); AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); @@ -347,7 +347,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR); } - private class TestPlayer : Visual.TestPlayer + private class LoadBlockingTestPlayer : TestPlayer { protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value); @@ -360,7 +360,7 @@ namespace osu.Game.Tests.Visual.Background public readonly Bindable ReplacesBackground = new Bindable(); public readonly Bindable IsPaused = new Bindable(); - public TestPlayer(bool allowPause = true) + public LoadBlockingTestPlayer(bool allowPause = true) : base(allowPause) { } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 4daab8d137..756f31e0bf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Storyboards; @@ -14,20 +13,22 @@ namespace osu.Game.Tests.Visual.Gameplay [Description("Player instantiated with an autoplay mod.")] public class TestSceneAutoplay : TestSceneAllRulesetPlayers { + protected new TestPlayer Player => (TestPlayer)base.Player; + private ClockBackedTestWorkingBeatmap.TrackVirtualManual track; protected override Player CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); - return new ScoreAccessiblePlayer(); + return new TestPlayer(false, false); } protected override void AddCheckSteps() { - AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); + AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); AddStep("rewind", () => track.Seek(-10000)); - AddUntilStep("key counter reset", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -38,18 +39,5 @@ namespace osu.Game.Tests.Visual.Gameplay return working; } - - private class ScoreAccessiblePlayer : TestPlayer - { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - public new HUDOverlay HUDOverlay => base.HUDOverlay; - - public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - - public ScoreAccessiblePlayer() - : base(false, false) - { - } - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 78c3b22fb9..310746d179 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -11,12 +10,8 @@ using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using osu.Game.Storyboards; using osuTK; @@ -24,8 +19,6 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneGameplayRewinding : PlayerTestScene { - private RulesetExposingPlayer player => (RulesetExposingPlayer)Player; - [Resolved] private AudioManager audioManager { get; set; } @@ -48,13 +41,13 @@ namespace osu.Game.Tests.Visual.Gameplay { AddUntilStep("wait for track to start running", () => track.IsRunning); addSeekStep(3000); - AddAssert("all judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); - AddUntilStep("key counter counted keys", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); - AddStep("clear results", () => player.AppliedResults.Clear()); + AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); + AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); - AddAssert("none judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); - AddUntilStep("key counters reset", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); - AddAssert("no results triggered", () => player.AppliedResults.Count == 0); + AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); + AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + AddAssert("no results triggered", () => Player.Results.Count == 0); } private void addSeekStep(double time) @@ -62,13 +55,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep($"seek to {time}", () => track.Seek(time)); // Allow a few frames of lenience - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } - protected override Player CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); - return new RulesetExposingPlayer(); + return base.CreatePlayer(ruleset); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) @@ -89,29 +82,5 @@ namespace osu.Game.Tests.Visual.Gameplay return beatmap; } - - private class RulesetExposingPlayer : Player - { - public readonly List AppliedResults = new List(); - - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - public new HUDOverlay HUDOverlay => base.HUDOverlay; - - public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - - public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; - - public RulesetExposingPlayer() - : base(false, false) - { - } - - [BackgroundDependencyLoader] - private void load() - { - ScoreProcessor.NewJudgement += r => AppliedResults.Add(r); - } - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index ad5bab4681..944e6ca6be 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osuTK; using osuTK.Input; @@ -282,14 +281,10 @@ namespace osu.Game.Tests.Visual.Gameplay protected override bool AllowFail => true; - protected override Player CreatePlayer(Ruleset ruleset) => new PausePlayer(); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PausePlayer(); protected class PausePlayer : TestPlayer { - public new HealthProcessor HealthProcessor => base.HealthProcessor; - - public new HUDOverlay HUDOverlay => base.HUDOverlay; - public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible; public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible; diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs index 3513b6c25a..a83320048b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -9,15 +9,12 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Gameplay { [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state. public class TestScenePauseWhenInactive : PlayerTestScene { - protected new TestPlayer Player => (TestPlayer)base.Player; - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = (Beatmap)base.CreateBeatmap(ruleset); @@ -46,6 +43,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("time of pause is after gameplay start time", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= Player.DrawableRuleset.GameplayStartTime); } - protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 100f99d130..175f909a5a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; @@ -307,17 +306,7 @@ namespace osu.Game.Tests.Visual.Gameplay public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; } - private class TestPlayer : Visual.TestPlayer - { - public new Bindable> Mods => base.Mods; - - public TestPlayer(bool allowPause = true, bool showResults = true) - : base(allowPause, showResults) - { - } - } - - protected class SlowLoadPlayer : Visual.TestPlayer + protected class SlowLoadPlayer : TestPlayer { public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(false); diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 7c5ba7d30f..eee31fe014 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -8,7 +8,6 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { @@ -16,7 +15,7 @@ namespace osu.Game.Tests.Visual { private readonly Ruleset ruleset; - protected Player Player; + protected TestPlayer Player; protected PlayerTestScene(Ruleset ruleset) { @@ -69,6 +68,6 @@ namespace osu.Game.Tests.Visual LoadScreen(Player); } - protected virtual Player CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false); + protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false); } } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 8e3821f1a0..f016d29f38 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -1,23 +1,51 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + /// + /// A player that exposes many components that would otherwise not be available, for testing purposes. + /// public class TestPlayer : Player { protected override bool PauseOnFocusLost { get; } public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; + /// + /// Mods from *player* (not OsuScreen). + /// + public new Bindable> Mods => base.Mods; + + public new HUDOverlay HUDOverlay => base.HUDOverlay; + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public new HealthProcessor HealthProcessor => base.HealthProcessor; + + public readonly List Results = new List(); + public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) : base(allowPause, showResults) { PauseOnFocusLost = pauseOnFocusLost; } + + [BackgroundDependencyLoader] + private void load() + { + ScoreProcessor.NewJudgement += r => Results.Add(r); + } } } From 26ce0d05d6f45f07cb6214b3f1036200f98513e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 11:33:30 +0900 Subject: [PATCH 086/387] Use autoplay mod rather than local replay provider --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 22 ++-------- osu.Game/Tests/Visual/ModTestScene.cs | 41 ++++++++----------- osu.Game/Tests/Visual/TestReplayPlayer.cs | 24 ----------- 3 files changed, 21 insertions(+), 66 deletions(-) delete mode 100644 osu.Game/Tests/Visual/TestReplayPlayer.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index e4b1e30bcd..0a98f49526 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -3,8 +3,6 @@ using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -17,11 +15,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestNoAdjustment() => CreateModTest(new ModTestData() + public void TestNoAdjustment() => CreateModTest(new ModTestData { Mod = new OsuModDifficultyAdjust(), Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 }); [Test] @@ -29,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }, Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 }); [Test] @@ -37,19 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }, Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 }); - - protected override TestPlayer CreateReplayPlayer(Score score, bool allowFail) => new ScoreAccessibleTestPlayer(score, allowFail); - - private class ScoreAccessibleTestPlayer : TestPlayer - { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - public ScoreAccessibleTestPlayer(Score score, bool allowFail) - : base(score, allowFail) - { - } - } } } diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 3d12001cca..9abe543bf6 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -3,13 +3,10 @@ using System; using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; -using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { @@ -27,52 +24,48 @@ namespace osu.Game.Tests.Visual { } - private ModTestData currentTest; + private ModTestData currentTestData; protected void CreateModTest(ModTestData testData) => CreateTest(() => { - AddStep("set test data", () => currentTest = testData); + AddStep("set test data", () => currentTestData = testData); }); public override void TearDownSteps() { AddUntilStep("test passed", () => { - if (currentTest == null) + if (currentTestData == null) return true; - return currentTest.PassCondition?.Invoke() ?? false; + return currentTestData.PassCondition?.Invoke() ?? false; }); base.TearDownSteps(); } - protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest?.Beatmap ?? base.CreateBeatmap(ruleset); + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); - protected sealed override Player CreatePlayer(Ruleset ruleset) + protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) { - SelectedMods.Value = SelectedMods.Value.Append(currentTest.Mod).ToArray(); + var mods = new List(SelectedMods.Value); - var score = currentTest.Autoplay - ? ruleset.GetAutoplayMod().CreateReplayScore(Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value)) - : null; + if (currentTestData.Mod != null) + mods.Add(currentTestData.Mod); + if (currentTestData.Autoplay) + mods.Add(ruleset.GetAutoplayMod()); - return CreateReplayPlayer(score, AllowFail); + SelectedMods.Value = mods; + + return new ModTestPlayer(AllowFail); } - /// - /// Creates the for a test case. - /// - /// The . - /// Whether the player can fail. - protected virtual TestPlayer CreateReplayPlayer(Score score, bool allowFail) => new TestPlayer(score, allowFail); - - protected class TestPlayer : TestReplayPlayer + protected class ModTestPlayer : TestPlayer { protected override bool AllowFail { get; } - public TestPlayer(Score score, bool allowFail) - : base(score, false, false) + public ModTestPlayer(bool allowFail) + : base(false, false) { AllowFail = allowFail; } diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs deleted file mode 100644 index e99fcc1e37..0000000000 --- a/osu.Game/Tests/Visual/TestReplayPlayer.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.UI; -using osu.Game.Scoring; -using osu.Game.Screens.Play; - -namespace osu.Game.Tests.Visual -{ - public class TestReplayPlayer : ReplayPlayer - { - protected override bool PauseOnFocusLost { get; } - - public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; - - public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - - public TestReplayPlayer(Score score, bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) - : base(score, allowPause, showResults) - { - PauseOnFocusLost = pauseOnFocusLost; - } - } -} From 9a12909f09a377f9348a496f0e95a52c2b51cedf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 11:53:04 +0900 Subject: [PATCH 087/387] Test ModDifficultyAdjust is actually taking effect --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 0a98f49526..69415b70e3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -1,8 +1,14 @@ // 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 NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -19,7 +25,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust(), Autoplay = true, - PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 + PassCondition = checkSomeHit + }); + + [Test] + public void TestCircleSize1() => CreateModTest(new ModTestData + { + Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 1 } }, + Autoplay = true, + PassCondition = () => checkSomeHit() && checkObjectsScale(0.78f) }); [Test] @@ -27,7 +41,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }, Autoplay = true, - PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 + PassCondition = () => checkSomeHit() && checkObjectsScale(0.15f) + }); + + [Test] + public void TestApproachRate1() => CreateModTest(new ModTestData + { + Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } }, + Autoplay = true, + PassCondition = () => checkSomeHit() && checkObjectsPreempt(1680) }); [Test] @@ -35,7 +57,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }, Autoplay = true, - PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 + PassCondition = () => checkSomeHit() && checkObjectsPreempt(450) }); + + private bool checkObjectsPreempt(double target) + { + var objects = Player.ChildrenOfType(); + if (!objects.Any()) + return false; + + return objects.All(o => o.HitObject.TimePreempt == target); + } + + private bool checkObjectsScale(float target) + { + var objects = Player.ChildrenOfType(); + if (!objects.Any()) + return false; + + return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType().First().Children.OfType().Single().Scale.X, target)); + } + + private bool checkSomeHit() + { + return Player.ScoreProcessor.JudgedHits >= 2; + } } } From 7229131d369c19ac8b54ab840c125e3bc83ee9a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 13:18:42 +0900 Subject: [PATCH 088/387] Fix song select max displayable star difficulty getting stuck at wrong maximum --- osu.Game/Configuration/OsuConfigManager.cs | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index ce959e9057..c5d68e4efe 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.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.Bindables; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; @@ -126,6 +127,34 @@ namespace osu.Game.Configuration public OsuConfigManager(Storage storage) : base(storage) { + Migrate(); + } + + public void Migrate() + { + // arrives as 2020.123.0 + var rawVersion = Get(OsuSetting.Version); + + if (rawVersion.Length < 6) + return; + + var pieces = rawVersion.Split('.'); + + if (!int.TryParse(pieces[0], out int year)) return; + if (!int.TryParse(pieces[1], out int monthDay)) return; + if (!int.TryParse(pieces[2], out int minor)) return; + + int combined = (year * 10000) + monthDay; + + if (combined < 20200305) + { + // the maximum value of this setting was changed. + // if we don't manually increase this, it causes song select to filter out beatmaps the user expects to see. + var maxStars = (BindableDouble)GetOriginalBindable(OsuSetting.DisplayStarsMaximum); + + if (maxStars.Value == 10) + maxStars.Value = maxStars.MaxValue; + } } public override TrackedSettings CreateTrackedSettings() => new TrackedSettings From 6477a7b73e1d7ede6f77cfb76b15e63d1be20bdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 13:34:04 +0900 Subject: [PATCH 089/387] Centralise creation of UpdateManagers --- osu.Android/OsuGameAndroid.cs | 7 +------ osu.Desktop/OsuGameDesktop.cs | 19 ++++++++++++------- osu.Game/OsuGame.cs | 4 ++++ osu.iOS/OsuGameIOS.cs | 7 ------- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index a91c010809..84f215f930 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -30,11 +30,6 @@ namespace osu.Android } } - protected override void LoadComplete() - { - base.LoadComplete(); - - Add(new SimpleUpdateManager()); - } + protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); } } \ No newline at end of file diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index f70cc24159..f05ee48914 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -47,20 +47,25 @@ namespace osu.Desktop return null; } + protected override UpdateManager CreateUpdateManager() + { + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.Windows: + return new SquirrelUpdateManager(); + + default: + return new SimpleUpdateManager(); + } + } + protected override void LoadComplete() { base.LoadComplete(); if (!noVersionOverlay) - { LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); - if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - Add(new SquirrelUpdateManager()); - else - Add(new SimpleUpdateManager()); - } - LoadComponentAsync(new DiscordRichPresence(), Add); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5781a7fbc4..916464ff53 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -43,6 +43,7 @@ using osu.Game.Overlays.Volume; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select; +using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; @@ -390,6 +391,8 @@ namespace osu.Game protected virtual Loader CreateLoader() => new Loader(); + protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); + protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); #region Beatmap progression @@ -528,6 +531,7 @@ namespace osu.Game AddRange(new Drawable[] { + CreateUpdateManager(), new VolumeControlReceptor { RelativeSizeAxes = Axes.Both, diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index e5ff4aec95..3a16f81530 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -11,12 +11,5 @@ namespace osu.iOS public class OsuGameIOS : OsuGame { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); - - protected override void LoadComplete() - { - base.LoadComplete(); - - Add(new UpdateManager()); - } } } From 74b5e76c0ebec3360fb998f4743e461329a12083 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 13:39:55 +0900 Subject: [PATCH 090/387] Fix dependency initialisation ordering --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 916464ff53..50aefb4dd2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -531,7 +531,6 @@ namespace osu.Game AddRange(new Drawable[] { - CreateUpdateManager(), new VolumeControlReceptor { RelativeSizeAxes = Axes.Both, @@ -632,6 +631,7 @@ namespace osu.Game chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; Add(externalLinkOpener = new ExternalLinkOpener()); + Add(CreateUpdateManager()); // dependency on notification overlay // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications }; From 1e6710020e3adc3da750730ab6f66802dc50488f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 13:46:25 +0900 Subject: [PATCH 091/387] Remove minor version for now --- osu.Game/Configuration/OsuConfigManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c5d68e4efe..5b20700086 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -142,7 +142,6 @@ namespace osu.Game.Configuration if (!int.TryParse(pieces[0], out int year)) return; if (!int.TryParse(pieces[1], out int monthDay)) return; - if (!int.TryParse(pieces[2], out int minor)) return; int combined = (year * 10000) + monthDay; From 1e1e8cbcb5915ad1d0db77d34926ebc54d0218aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 14:46:38 +0900 Subject: [PATCH 092/387] Always update version --- osu.Game/Updater/UpdateManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 48505a9891..f7a7795d9b 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -34,12 +34,12 @@ namespace osu.Game.Updater if (game.IsDeployedBuild && version != lastVersion) { - config.Set(OsuSetting.Version, version); - // only show a notification if we've previously saved a version to the config file (ie. not the first run). if (!string.IsNullOrEmpty(lastVersion)) Notifications.Post(new UpdateCompleteNotification(version)); } + + config.Set(OsuSetting.Version, version); } private class UpdateCompleteNotification : SimpleNotification From a311ace62656d854b8136c93ad7f7dcb91e1e9ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 14:46:07 +0900 Subject: [PATCH 093/387] Add migration test --- .../Visual/Navigation/OsuGameTestScene.cs | 23 +++++++---- .../Navigation/TestSettingsMigration.cs | 41 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 70d71d0952..b0bfb64d61 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -62,14 +62,7 @@ namespace osu.Game.Tests.Visual.Navigation var frameworkConfig = host.Dependencies.Get(); frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity).Disabled = false; - Game = new TestOsuGame(LocalStorage, API); - Game.SetHost(host); - - // todo: this can be removed once we can run audio tracks without a device present - // see https://github.com/ppy/osu/issues/1302 - Game.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles); - - Add(Game); + CreateGame(); }); AddUntilStep("Wait for load", () => Game.IsLoaded); @@ -78,6 +71,18 @@ namespace osu.Game.Tests.Visual.Navigation ConfirmAtMainMenu(); } + protected void CreateGame() + { + Game = new TestOsuGame(LocalStorage, API); + Game.SetHost(host); + + // todo: this can be removed once we can run audio tracks without a device present + // see https://github.com/ppy/osu/issues/1302 + Game.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles); + + Add(Game); + } + protected void PushAndConfirm(Func newScreen) { Screen screen = null; @@ -103,6 +108,8 @@ namespace osu.Game.Tests.Visual.Navigation public new Bindable Ruleset => base.Ruleset; + public override string Version => "test game"; + protected override Loader CreateLoader() => new TestLoader(); public new void PerformFromScreen(Action action, IEnumerable validScreens = null) => base.PerformFromScreen(action, validScreens); diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs new file mode 100644 index 0000000000..c0b77b580e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Configuration; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSettingsMigration : OsuGameTestScene + { + public override void RecycleLocalStorage() + { + base.RecycleLocalStorage(); + + using (var config = new OsuConfigManager(LocalStorage)) + { + config.Set(OsuSetting.Version, "2020.101.0"); + config.Set(OsuSetting.DisplayStarsMaximum, 10.0); + } + } + + [Test] + public void TestDisplayStarsMigration() + { + AddAssert("config has migrated value", () => Precision.AlmostEquals(Game.LocalConfig.Get(OsuSetting.DisplayStarsMaximum), 10.1)); + + AddStep("set value again", () => Game.LocalConfig.Set(OsuSetting.DisplayStarsMaximum, 10)); + + AddStep("force save config", () => Game.LocalConfig.Save()); + + AddStep("remove game", () => Remove(Game)); + + AddStep("create game again", CreateGame); + + AddUntilStep("Wait for load", () => Game.IsLoaded); + + AddAssert("config did not migrate value", () => Precision.AlmostEquals(Game.LocalConfig.Get(OsuSetting.DisplayStarsMaximum), 10)); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a890331f05..33333e592e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -97,7 +97,7 @@ namespace osu.Game public bool IsDeployedBuild => AssemblyVersion.Major > 0; - public string Version + public virtual string Version { get { diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index b203557fab..f102e2ece3 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual base.Content.Add(content = new DrawSizePreservingFillContainer()); } - public void RecycleLocalStorage() + public virtual void RecycleLocalStorage() { if (localStorage?.IsValueCreated == true) { From 507af4fa72cb9c7e98733eb5925198ab4caac756 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 15:35:27 +0900 Subject: [PATCH 094/387] Add comment about rationale behind always updating version in config --- osu.Game/Updater/UpdateManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index f7a7795d9b..28a295215f 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -39,6 +39,8 @@ namespace osu.Game.Updater Notifications.Post(new UpdateCompleteNotification(version)); } + // debug / local compilations will reset to a non-release string. + // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations). config.Set(OsuSetting.Version, version); } From 5b8037ea7d2b69e29ae82eb54aba16e2bd07efe9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 15:36:36 +0900 Subject: [PATCH 095/387] Add note about early migration return on non-release transitions --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 5b20700086..21de654670 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -140,6 +140,8 @@ namespace osu.Game.Configuration var pieces = rawVersion.Split('.'); + // on a fresh install or when coming from a non-release build, execution will end here. + // we don't want to run migrations in such cases. if (!int.TryParse(pieces[0], out int year)) return; if (!int.TryParse(pieces[1], out int monthDay)) return; From 646c8fe077eca5b6630b397e38fc5b3c6330e7ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 15:40:48 +0900 Subject: [PATCH 096/387] Add note about version override --- osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index b0bfb64d61..ea8a06e990 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -108,6 +108,7 @@ namespace osu.Game.Tests.Visual.Navigation public new Bindable Ruleset => base.Ruleset; + // if we don't do this, when running under nUnit the version that gets populated is that of nUnit. public override string Version => "test game"; protected override Loader CreateLoader() => new TestLoader(); From 9307caa3bfb60e7636033fe78311549da1653487 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Mar 2020 16:58:07 +0900 Subject: [PATCH 097/387] Fix typos --- osu.Game/OsuGame.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0be9e6cdaa..e54bbaabb2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -410,7 +410,7 @@ namespace osu.Game void trackCompleted(WorkingBeatmap b) { - // the source of track completion is the audio thread, so the beatmap may have changed before a firing. + // the source of track completion is the audio thread, so the beatmap may have changed before firing. if (Beatmap.Value != b) return; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 67aa4a8d4d..1048b37348 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -214,7 +214,7 @@ namespace osu.Game // ScheduleAfterChildren is safety against something in the current frame accessing the previous beatmap's track // and potentially causing a reload of it after just unloading. - // Note that the reason for this being added *has* been resolved, so it may be feasible to remover this if required. + // Note that the reason for this being added *has* been resolved, so it may be feasible to removed this if required. Beatmap.BindValueChanged(b => ScheduleAfterChildren(() => { // compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) From 0c1775b52281517e8f6f0518fc930b42b0f26aca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Mar 2020 17:12:14 +0900 Subject: [PATCH 098/387] Fix incorrect condition and add test --- .../Visual/Navigation/OsuGameTestScene.cs | 2 ++ .../Navigation/TestSceneScreenNavigation.cs | 16 ++++++++++++++++ osu.Game/OsuGame.cs | 10 +++++----- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 70d71d0952..e984806dc9 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -97,6 +97,8 @@ namespace osu.Game.Tests.Visual.Navigation public new SettingsPanel Settings => base.Settings; + public new MusicController MusicController => base.MusicController; + public new OsuConfigManager LocalConfig => base.LocalConfig; public new Bindable Beatmap => base.Beatmap; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8258cc9465..9d603ac471 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -114,6 +114,22 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Options overlay was closed", () => Game.Settings.State.Value == Visibility.Hidden); } + [Test] + public void TestWaitForNextTrackInMenu() + { + bool trackCompleted = false; + + AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); + AddStep("Seek close to end", () => + { + Game.MusicController.SeekTo(Game.Beatmap.Value.Track.Length - 1000); + Game.Beatmap.Value.Track.Completed += () => trackCompleted = true; + }); + + AddUntilStep("Track was completed", () => trackCompleted); + AddUntilStep("Track was restarted", () => Game.Beatmap.Value.Track.IsRunning); + } + private void pushEscape() => AddStep("Press escape", () => pressAndRelease(Key.Escape)); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e54bbaabb2..19602d524e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -414,8 +414,8 @@ namespace osu.Game if (Beatmap.Value != b) return; - if (Beatmap.Value.Track.Looping && !Beatmap.Disabled) - musicController.NextTrack(); + if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled) + MusicController.NextTrack(); } } @@ -588,7 +588,7 @@ namespace osu.Game loadComponentSingleFile(new OnScreenDisplay(), Add, true); - loadComponentSingleFile(musicController = new MusicController(), Add, true); + loadComponentSingleFile(MusicController = new MusicController(), Add, true); loadComponentSingleFile(notifications = new NotificationOverlay { @@ -896,7 +896,7 @@ namespace osu.Game private ScalingContainer screenContainer; - private MusicController musicController; + protected MusicController MusicController { get; private set; } protected override bool OnExiting() { @@ -954,7 +954,7 @@ namespace osu.Game { OverlayActivationMode.Value = newOsuScreen.InitialOverlayActivationMode; - musicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments; + MusicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments; if (newOsuScreen.HideOverlaysOnEnter) CloseAllOverlays(); From 371a54364508eb84a37f1e098bded8584e2c1d8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 20:25:38 +0900 Subject: [PATCH 099/387] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1c4a6ffe75..97f7a7edb1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4d59b709aa..855bda3679 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6897d3e625..e2c4c09047 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 332f56a7f8f62fa426e1f153cb184a7030658961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 20:34:24 +0900 Subject: [PATCH 100/387] Fix nullref in tests --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index c110608a0e..8c264ce974 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - if (Beatmap.BeatmapSet.Equals(criteria.SelectedBeatmapSet)) + if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { // bypass filtering for selected beatmap Filtered.Value = false; From 0477ef6c130052d0dc95d7fc78e0c2cc2e8a9a30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 21:45:19 +0900 Subject: [PATCH 101/387] Force a selection after filtering to ensure correct difficulty is selected --- osu.Game/Screens/Select/SongSelect.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6577ed8506..528222a89c 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -397,6 +397,8 @@ namespace osu.Game.Screens.Select var criteria = FilterControl.CreateCriteria(); criteria.SelectedBeatmapSet = e.NewValue.BeatmapInfo.BeatmapSet; Carousel.Filter(criteria); + + Carousel.SelectBeatmap(e.NewValue.BeatmapInfo); } } From 583e2c3f4a616fea8d2b9abc7065135e917217d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 00:10:05 +0900 Subject: [PATCH 102/387] Actually check rate is applied --- .../Mods/TestSceneOsuModDoubleTime.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs index deb733c581..dcf19ad993 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -2,14 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModDoubleTime : ModSandboxTestScene + public class TestSceneOsuModDoubleTime : ModTestScene { public TestSceneOsuModDoubleTime() : base(new OsuRuleset()) @@ -21,21 +20,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [TestCase(1.5)] [TestCase(2)] [TestCase(5)] - public void TestDefaultRate(double rate) => CreateModTest(new ModTestCaseData("1.5x", new OsuModDoubleTime { SpeedChange = { Value = rate } }) + public void TestSpeedChangeCustomisation(double rate) { - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 - }); + var mod = new OsuModDoubleTime { SpeedChange = { Value = rate } }; - protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); - - private class ScoreAccessibleTestPlayer : TestPlayer - { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - public ScoreAccessibleTestPlayer(Score score) - : base(score) + CreateModTest(new ModTestData { - } + Mod = mod, + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 && + Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value) + }); } } } From ece263131b6c4d89a9580c70f34fdf36837b4c4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 00:36:05 +0900 Subject: [PATCH 103/387] Update to follow new naming/structure --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 20 +++++++------------- osu.Game/Tests/Visual/ModTestScene.cs | 2 +- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 31d2ce9281..4bc00425bf 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -1,29 +1,28 @@ // 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.Extensions.TypeExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Scoring; namespace osu.Game.Tests.Visual { - public abstract class ModPerfectTestScene : ModSandboxTestScene + public abstract class ModPerfectTestScene : ModTestScene { private readonly Ruleset ruleset; - private readonly ModPerfect perfectMod; + private readonly ModPerfect mod; - protected ModPerfectTestScene(Ruleset ruleset, ModPerfect perfectMod) + protected ModPerfectTestScene(Ruleset ruleset, ModPerfect mod) : base(ruleset) { this.ruleset = ruleset; - this.perfectMod = perfectMod; + this.mod = mod; } - protected void CreateHitObjectTest(HitObjectTestCase testCaseData, bool shouldMiss) => CreateModTest(new ModTestCaseData(testCaseData.HitObject.GetType().ReadableName(), perfectMod) + protected void CreateHitObjectTest(HitObjectTestCase testCaseData, bool shouldMiss) => CreateModTest(new ModTestData { + Mod = mod, Beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo }, @@ -33,15 +32,10 @@ namespace osu.Game.Tests.Visual PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testCaseData.FailOnMiss) }); - protected sealed override TestPlayer CreateReplayPlayer(Score score) => new PerfectModTestPlayer(score); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PerfectModTestPlayer(); private class PerfectModTestPlayer : TestPlayer { - public PerfectModTestPlayer(Score score) - : base(score) - { - } - protected override bool AllowFail => true; public bool CheckFailed(bool failed) diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 9abe543bf6..eb418304d9 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); - protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { var mods = new List(SelectedMods.Value); From e3509c742c4ec1bb9df56d4a3c1f733ad4587f58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 01:28:59 +0900 Subject: [PATCH 104/387] Track time in a simpler way in TrackVirtualManual --- osu.Game/Tests/Visual/OsuTestScene.cs | 47 ++++++++++++++++++++------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 917d12ebb1..3c95b990e1 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -229,10 +229,10 @@ namespace osu.Game.Tests.Visual /// public class TrackVirtualManual : Track { - private readonly StopwatchClock stopwatchClock = new StopwatchClock(); - private readonly IFrameBasedClock referenceClock; + private bool running; + public TrackVirtualManual(IFrameBasedClock referenceClock) { this.referenceClock = referenceClock; @@ -241,32 +241,55 @@ namespace osu.Game.Tests.Visual public override bool Seek(double seek) { - var offset = Math.Clamp(seek, 0, Length); + accumulated = Math.Min(seek, Length); + lastReferenceTime = null; - stopwatchClock.Seek(offset); - - return offset == seek; + return accumulated == seek; } - public override void Start() => stopwatchClock.Start(); + public override void Start() + { + running = true; + } public override void Reset() { - stopwatchClock.Seek(0); + Seek(0); base.Reset(); } - public override void Stop() => stopwatchClock.Stop(); + public override void Stop() + { + if (running) + { + running = false; + lastReferenceTime = null; + } + } - public override bool IsRunning => stopwatchClock.IsRunning; + public override bool IsRunning => running; - public override double CurrentTime => stopwatchClock.CurrentTime; + private double? lastReferenceTime; + + private double accumulated; + + public override double CurrentTime => Math.Min(accumulated, Length); protected override void UpdateState() { base.UpdateState(); - stopwatchClock.Rate = Rate * referenceClock.Rate; + if (running) + { + double refTime = referenceClock.CurrentTime; + + if (lastReferenceTime.HasValue) + accumulated += (refTime - lastReferenceTime.Value) * Rate; + + lastReferenceTime = refTime; + } + + Console.WriteLine($"t={CurrentTime}"); if (CurrentTime >= Length) { From ebc86c10754ede8686e06b06682c272ff117b562 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 02:08:49 +0900 Subject: [PATCH 105/387] Fix random test failure --- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3daf5b1ff1..ac7e509c2c 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Play { base.Update(); - var progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); + var progress = fadeOutBeginTime <= displayTime ? 1 : Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); From c3ad08f230b635f4dcc8582c5ee2028f04cd25c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 02:18:35 +0900 Subject: [PATCH 106/387] Remove wild writeline --- osu.Game/Tests/Visual/OsuTestScene.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 3c95b990e1..5623435da4 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -289,8 +289,6 @@ namespace osu.Game.Tests.Visual lastReferenceTime = refTime; } - Console.WriteLine($"t={CurrentTime}"); - if (CurrentTime >= Length) { Stop(); From bd1dbea6f4b35937777ef3f5b017d2bf787e35e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Mar 2020 23:10:14 +0100 Subject: [PATCH 107/387] Centralise background colour updates --- .../Profile/Sections/ProfileItemContainer.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs index 1ab9ed3f82..c057ebe12b 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs @@ -27,8 +27,7 @@ namespace osu.Game.Overlays.Profile.Sections set { idleColour = value; - if (!IsHovered) - background.Colour = value; + fadeBackgroundColour(); } } @@ -40,8 +39,7 @@ namespace osu.Game.Overlays.Profile.Sections set { hoverColour = value; - if (IsHovered) - background.Colour = value; + fadeBackgroundColour(); } } @@ -73,14 +71,19 @@ namespace osu.Game.Overlays.Profile.Sections protected override bool OnHover(HoverEvent e) { - background.FadeColour(HoverColour, hover_duration, Easing.OutQuint); + fadeBackgroundColour(hover_duration); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - background.FadeColour(IdleColour, hover_duration, Easing.OutQuint); + fadeBackgroundColour(hover_duration); + } + + private void fadeBackgroundColour(double fadeDuration = 0) + { + background.FadeColour(IsHovered ? HoverColour : IdleColour, fadeDuration, Easing.OutQuint); } } } From 5b0846cb69a2f5aca2da311d179169fc1c0f5deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Mar 2020 23:15:53 +0100 Subject: [PATCH 108/387] Handle hover explicitly --- osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs index c057ebe12b..afa6bd9f79 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Profile.Sections protected override bool OnHover(HoverEvent e) { fadeBackgroundColour(hover_duration); - return base.OnHover(e); + return true; } protected override void OnHoverLost(HoverLostEvent e) From ac88ba717b7f6589b01950ac915a4801eac4a007 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:03:26 +0900 Subject: [PATCH 109/387] Ensure screens respect aspect ratio in tests --- osu.Game.Tournament/Screens/TournamentScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tournament/Screens/TournamentScreen.cs b/osu.Game.Tournament/Screens/TournamentScreen.cs index 0b5b3e728b..5da7c7a5d2 100644 --- a/osu.Game.Tournament/Screens/TournamentScreen.cs +++ b/osu.Game.Tournament/Screens/TournamentScreen.cs @@ -18,6 +18,9 @@ namespace osu.Game.Tournament.Screens protected TournamentScreen() { RelativeSizeAxes = Axes.Both; + + FillMode = FillMode.Fit; + FillAspectRatio = 16 / 9f; } public override void Hide() => this.FadeOut(FADE_DELAY); From 8ff3370273fb9c857e0117bab3235b6408caf6c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:27:38 +0900 Subject: [PATCH 110/387] Add a short load delay for avatars to avoid unnecessary fetching --- osu.Game/Users/Drawables/UpdateableAvatar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 59fbb5f910..171462f3fc 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -43,6 +43,8 @@ namespace osu.Game.Users.Drawables set => base.EdgeEffect = value; } + protected override double LoadDelay => 200; + /// /// Whether to show a default guest representation on null user (as opposed to nothing). /// From 88759e65a0b2702defc8e036561ad2d18fbb02dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:31:36 +0900 Subject: [PATCH 111/387] Remove layout durations from tournament editor screns for better performance --- osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs | 2 -- osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs | 2 -- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 2 -- osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs | 2 -- 4 files changed, 8 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 7119533743..8b8078e119 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -129,8 +129,6 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, ChildrenEnumerable = round.Beatmaps.Select(p => new RoundBeatmapRow(round, p)) }; } diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index e68946aaf2..46bb7b83e3 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -124,8 +124,6 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, ChildrenEnumerable = round.Beatmaps.Select(p => new SeedingBeatmapRow(round, p)) }; } diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index ca8bce1cca..631393c6f4 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -202,8 +202,6 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, ChildrenEnumerable = team.Players.Select(p => new PlayerRow(team, p)) }; } diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 5598910824..e4256e727d 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -48,8 +48,6 @@ namespace osu.Game.Tournament.Screens.Editors Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, Spacing = new Vector2(20) }, }, From 40074f10dbd5999dca3f38dc0a6de093c28de463 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 17:55:05 +0900 Subject: [PATCH 112/387] Remove unnecessary override --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 9140cccafd..2a8f77210a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -32,12 +32,5 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } - - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - { - var working = base.CreateWorkingBeatmap(beatmap, storyboard); - - return working; - } } } From 3b0e3cd71a3ae257ec3f21f774c1ba2c63cf8e53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 17:55:57 +0900 Subject: [PATCH 113/387] Remove using statements --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 2a8f77210a..afeda5fb7c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -3,10 +3,8 @@ using System.ComponentModel; using System.Linq; -using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Screens.Play; -using osu.Game.Storyboards; namespace osu.Game.Tests.Visual.Gameplay { From 0ccf691c972f3de8253c357b101e2f5c8ba5e93d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 18:00:07 +0900 Subject: [PATCH 114/387] Remove unnecessary interpolation --- osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 10827bc0b9..227ada70fe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2); - AddStep($"Disable counting", () => testCounter.IsCounting = false); + AddStep("Disable counting", () => testCounter.IsCounting = false); addPressKeyStep(); AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2); From a59c3d997da57bcfa33e2f3342222ccaa825d674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 18:00:17 +0900 Subject: [PATCH 115/387] Refactor implementation to better match what already existed --- osu.Game/Screens/Play/Player.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2d49c707ec..bcadba14af 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -157,10 +157,7 @@ namespace osu.Game.Screens.Play addGameplayComponents(GameplayClockContainer, Beatmap.Value); addOverlayComponents(GameplayClockContainer, Beatmap.Value); - DrawableRuleset.HasReplayLoaded.BindValueChanged(e => - { - updatePauseOnFocusLostState(e.NewValue, BreakOverlay.IsBreakTime.Value); - }, true); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); @@ -232,7 +229,11 @@ namespace osu.Game.Screens.Play IsPaused = { BindTarget = GameplayClockContainer.IsPaused } }, PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, - KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, IsCounting = false }, + KeyCounter = + { + AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, + IsCounting = false + }, RequestSeek = GameplayClockContainer.Seek, Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -289,16 +290,16 @@ namespace osu.Game.Screens.Play HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } - private void onBreakTimeChanged(ValueChangedEvent changeEvent) + private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { - updatePauseOnFocusLostState(DrawableRuleset.HasReplayLoaded.Value, changeEvent.NewValue); - HUDOverlay.KeyCounter.IsCounting = !changeEvent.NewValue; + updatePauseOnFocusLostState(); + HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; } - private void updatePauseOnFocusLostState(bool replayLoaded, bool isBreakTime) => + private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost - && !replayLoaded - && !isBreakTime; + && !DrawableRuleset.HasReplayLoaded.Value + && !BreakOverlay.IsBreakTime.Value; private IBeatmap loadPlayableBeatmap() { From 2d95f2992534444949ebbf6a66dd53b333a94c9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:06:23 +0900 Subject: [PATCH 116/387] Add gameplay screen specific video --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 6a3095d42d..c74302a869 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.Components; @@ -19,7 +20,7 @@ using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay { - public class GameplayScreen : BeatmapInfoScreen + public class GameplayScreen : BeatmapInfoScreen, IProvideVideo { private readonly BindableBool warmup = new BindableBool(); @@ -39,12 +40,17 @@ namespace osu.Game.Tournament.Screens.Gameplay private TournamentMatchChatDisplay chat { get; set; } [BackgroundDependencyLoader] - private void load(LadderInfo ladder, MatchIPCInfo ipc) + private void load(LadderInfo ladder, MatchIPCInfo ipc, Storage storage) { this.ipc = ipc; AddRangeInternal(new Drawable[] { + new TourneyVideo(storage.GetStream("videos/gameplay.m4v")) + { + Loop = true, + RelativeSizeAxes = Axes.Both, + }, new MatchHeader(), new Container { From 0a72fa69ab70bbb14fa7742e19a6d0075aefa21f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 18:38:29 +0900 Subject: [PATCH 117/387] Simplify video creation (and handle fallback better) --- .../Components/TourneyVideo.cs | 38 ++++++++++++------- .../Screens/Gameplay/GameplayScreen.cs | 2 +- .../Screens/Ladder/LadderScreen.cs | 2 +- .../Screens/Schedule/ScheduleScreen.cs | 4 +- .../Screens/TeamIntro/SeedingScreen.cs | 2 +- .../Screens/TeamIntro/TeamIntroScreen.cs | 2 +- .../Screens/TeamWin/TeamWinScreen.cs | 4 +- osu.Game.Tournament/TournamentSceneManager.cs | 2 +- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 206689ca1a..7d2eaff515 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -1,12 +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 System.IO; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Video; +using osu.Framework.Platform; using osu.Framework.Timing; using osu.Game.Graphics; @@ -14,21 +15,24 @@ namespace osu.Game.Tournament.Components { public class TourneyVideo : CompositeDrawable { - private readonly VideoSprite video; + private readonly string filename; + private readonly bool drawFallbackGradient; + private VideoSprite video; - private readonly ManualClock manualClock; + private ManualClock manualClock; - public TourneyVideo(Stream stream) + public TourneyVideo(string filename, bool drawFallbackGradient = false) { - if (stream == null) - { - InternalChild = new Box - { - Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.3f), OsuColour.Gray(0.6f)), - RelativeSizeAxes = Axes.Both, - }; - } - else + this.filename = filename; + this.drawFallbackGradient = drawFallbackGradient; + } + + [BackgroundDependencyLoader] + private void load(Storage storage) + { + var stream = storage.GetStream($@"videos/{filename}.m4v"); + + if (stream != null) { InternalChild = video = new VideoSprite(stream) { @@ -37,6 +41,14 @@ namespace osu.Game.Tournament.Components Clock = new FramedClock(manualClock = new ManualClock()) }; } + else if (drawFallbackGradient) + { + InternalChild = new Box + { + Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.3f), OsuColour.Gray(0.6f)), + RelativeSizeAxes = Axes.Both, + }; + } } public bool Loop diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index c74302a869..d632e7c5f3 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tournament.Screens.Gameplay AddRangeInternal(new Drawable[] { - new TourneyVideo(storage.GetStream("videos/gameplay.m4v")) + new TourneyVideo("gameplay") { Loop = true, RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 293f6e0068..7b265ded32 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tournament.Screens.Ladder RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new TourneyVideo(storage.GetStream(@"videos/ladder.m4v")) + new TourneyVideo("ladder") { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 080570eac4..4c93c04fcf 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Schedule { - public class ScheduleScreen : TournamentScreen, IProvideVideo + public class ScheduleScreen : TournamentScreen { private readonly Bindable currentMatch = new Bindable(); private Container mainContainer; @@ -33,7 +33,7 @@ namespace osu.Game.Tournament.Screens.Schedule InternalChildren = new Drawable[] { - new TourneyVideo(storage.GetStream(@"videos/schedule.m4v")) + new TourneyVideo("schedule") { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index db5363c155..513d84b594 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro InternalChildren = new Drawable[] { - new TourneyVideo(storage.GetStream(@"videos/seeding.m4v")) + new TourneyVideo("seeding") { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index 6559113f55..d584c21058 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro InternalChildren = new Drawable[] { - new TourneyVideo(storage.GetStream(@"videos/teamintro.m4v")) + new TourneyVideo("teamintro") { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 30b86f8421..1765ab7ba2 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -31,13 +31,13 @@ namespace osu.Game.Tournament.Screens.TeamWin InternalChildren = new Drawable[] { - blueWinVideo = new TourneyVideo(storage.GetStream(@"videos/teamwin-blue.m4v")) + blueWinVideo = new TourneyVideo("teamwin-blue") { Alpha = 1, RelativeSizeAxes = Axes.Both, Loop = true, }, - redWinVideo = new TourneyVideo(storage.GetStream(@"videos/teamwin-red.m4v")) + redWinVideo = new TourneyVideo("teamwin-red") { Alpha = 0, RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 9f5f2b6827..287e25b1fb 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tournament //Masking = true, Children = new Drawable[] { - video = new TourneyVideo(storage.GetStream("videos/main.m4v")) + video = new TourneyVideo("main", true) { Loop = true, RelativeSizeAxes = Axes.Both, From 16cc49daa00d3c4af18130162ccee7f74a1bf098 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 18:47:31 +0900 Subject: [PATCH 118/387] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1c4a6ffe75..97f7a7edb1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4d59b709aa..855bda3679 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6897d3e625..e2c4c09047 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 3295f8657ac3d74a95c10016273068fbbdedde26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 22:44:11 +0900 Subject: [PATCH 119/387] Restore clamp behaviour --- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 5623435da4..d1d8059cb1 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -241,7 +241,7 @@ namespace osu.Game.Tests.Visual public override bool Seek(double seek) { - accumulated = Math.Min(seek, Length); + accumulated = Math.Clamp(seek, 0, Length); lastReferenceTime = null; return accumulated == seek; From 491840b17d215cd983dca0badaac0a502e6d0d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Mar 2020 21:10:23 +0100 Subject: [PATCH 120/387] Add failing tests --- .../TestSceneHitCircleArea.cs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs new file mode 100644 index 0000000000..06eccdafdb --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.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; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneHitCircleArea : ManualInputManagerTestScene + { + private HitCircle hitCircle; + private DrawableHitCircle drawableHitCircle; + private DrawableHitCircle.HitReceptor hitAreaReceptor => drawableHitCircle.HitArea; + + [SetUp] + public new void SetUp() + { + base.SetUp(); + + Schedule(() => + { + hitCircle = new HitCircle + { + Position = new Vector2(100, 100), + StartTime = Time.Current + 500 + }; + + hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + Child = new SkinProvidingContainer(new DefaultSkin()) + { + RelativeSizeAxes = Axes.Both, + Child = drawableHitCircle = new DrawableHitCircle(hitCircle) + { + Size = new Vector2(100) + } + }; + }); + } + + [Test] + public void TestCircleHitCentre() + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(hitAreaReceptor.ScreenSpaceDrawQuad.Centre)); + scheduleHit(); + + AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); + } + + [Test] + public void TestCircleHitLeftEdge() + { + AddStep("move mouse to left edge", () => + { + var drawQuad = hitAreaReceptor.ScreenSpaceDrawQuad; + var mousePosition = new Vector2(drawQuad.TopLeft.X, drawQuad.Centre.Y); + + InputManager.MoveMouseTo(mousePosition); + }); + scheduleHit(); + + AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); + } + + [Test] + public void TestCircleHitTopLeftEdge() + { + AddStep("move mouse to top left circle edge", () => + { + var drawQuad = hitAreaReceptor.ScreenSpaceDrawQuad; + // sqrt(2) / 2 = sin(45deg) = cos(45deg) + // draw width halved to get radius + // 0.95f taken for leniency + float correction = 0.95f * (float)Math.Sqrt(2) / 2 * (drawQuad.Width / 2); + var mousePosition = new Vector2(drawQuad.Centre.X - correction, drawQuad.Centre.Y - correction); + + InputManager.MoveMouseTo(mousePosition); + }); + scheduleHit(); + + AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); + } + + [Test] + public void TestCircleMissBoundingBoxCorner() + { + AddStep("move mouse to top left corner of bounding box", () => InputManager.MoveMouseTo(hitAreaReceptor.ScreenSpaceDrawQuad.TopLeft)); + scheduleHit(); + + AddAssert("hit not registered", () => hitAreaReceptor.HitAction == null); + } + + private void scheduleHit() => AddStep("schedule action", () => + { + var delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current; + Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(OsuAction.LeftButton), delay); + }); + } +} From 77fd7480352b3dbc834fe7fa85b01d17491466c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Mar 2020 21:21:20 +0100 Subject: [PATCH 121/387] Fix incorrect circle piece hitbox Hitboxes of circle pieces in osu! have regressed with commit 8592335. The reason for the regression was that hit detection was moved from DrawableHitCircle itself to a newly-introduced private HitArea class (now named HitReceptor). As HitArea inherited from Drawable, it would return IsHovered == true over its entire bounding box. This meant that the hit area could wrongly pick up actions that are not within circle radius and make them into hits. To resolve, make HitReceptor a CompositeDrawable and set its corner radius to match the circle piece. This fixes the invalid hitbox, as IsHovered takes radius into account. --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 4ef63bb2a0..da1e666aba 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => ApproachCircle; - public class HitReceptor : Drawable, IKeyBindingHandler + public class HitReceptor : CompositeDrawable, IKeyBindingHandler { // IsHovered is used public override bool HandlePositionalInput => true; @@ -185,6 +185,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre; Origin = Anchor.Centre; + + CornerRadius = OsuHitObject.OBJECT_RADIUS; + CornerExponent = 2; } public bool OnPressed(OsuAction action) From b60876455481d8dd756d938fe07865628b872128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Mar 2020 22:09:02 +0100 Subject: [PATCH 122/387] Cover area just outside circle in test --- osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 06eccdafdb..67b6dac787 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -71,23 +71,23 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); } - [Test] - public void TestCircleHitTopLeftEdge() + [TestCase(0.95f, OsuAction.LeftButton)] + [TestCase(1.05f, null)] + public void TestHitsCloseToEdge(float relativeDistanceFromCentre, OsuAction? expectedAction) { AddStep("move mouse to top left circle edge", () => { var drawQuad = hitAreaReceptor.ScreenSpaceDrawQuad; // sqrt(2) / 2 = sin(45deg) = cos(45deg) // draw width halved to get radius - // 0.95f taken for leniency - float correction = 0.95f * (float)Math.Sqrt(2) / 2 * (drawQuad.Width / 2); + float correction = relativeDistanceFromCentre * (float)Math.Sqrt(2) / 2 * (drawQuad.Width / 2); var mousePosition = new Vector2(drawQuad.Centre.X - correction, drawQuad.Centre.Y - correction); InputManager.MoveMouseTo(mousePosition); }); scheduleHit(); - AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); + AddAssert($"hit {(expectedAction == null ? "not " : string.Empty)}registered", () => hitAreaReceptor.HitAction == expectedAction); } [Test] From e886c155e60d103dcc232bc210d0eb7017f6664f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 7 Mar 2020 04:05:17 +0300 Subject: [PATCH 123/387] Adjust text size values --- osu.Game/Users/UserPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index a900a55dab..5676113aad 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -129,7 +129,7 @@ namespace osu.Game.Users protected OsuSpriteText CreateUsername() => new OsuSpriteText { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), Shadow = false, Text = User.Username, }; @@ -150,7 +150,7 @@ namespace osu.Game.Users var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft; - statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold)).With(text => + statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)).With(text => { text.Anchor = alignment; text.Origin = alignment; @@ -171,7 +171,7 @@ namespace osu.Game.Users { Anchor = alignment, Origin = alignment, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) }); return statusContainer; From 9f44a7b2ce404a7f4c363a7643e779896691924d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 8 Mar 2020 03:07:14 +0300 Subject: [PATCH 124/387] Simplify status assignment in the test scene --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index f02a570f4f..ccae778745 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online }; private readonly Bindable activity = new Bindable(); + private readonly Bindable status = new Bindable(); private UserGridPanel peppy; private UserListPanel evast; @@ -76,20 +77,20 @@ namespace osu.Game.Tests.Visual.Online flyte.Status.Value = new UserStatusOnline(); - peppy.Status.Value = null; + peppy.Status.BindTo(status); peppy.Activity.BindTo(activity); - evast.Status.Value = null; + evast.Status.BindTo(status); evast.Activity.BindTo(activity); }); [Test] public void TestUserStatus() { - AddStep("online", () => peppy.Status.Value = evast.Status.Value = new UserStatusOnline()); - AddStep("do not disturb", () => peppy.Status.Value = evast.Status.Value = new UserStatusDoNotDisturb()); - AddStep("offline", () => peppy.Status.Value = evast.Status.Value = new UserStatusOffline()); - AddStep("null status", () => peppy.Status.Value = evast.Status.Value = null); + AddStep("online", () => status.Value = new UserStatusOnline()); + AddStep("do not disturb", () => status.Value = new UserStatusDoNotDisturb()); + AddStep("offline", () => status.Value = new UserStatusOffline()); + AddStep("null status", () => status.Value = null); } [Test] From 3caffb81e187e2d546fba85640791004d12e3ad5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:03:18 +0900 Subject: [PATCH 125/387] Add new element colours to TournamentGame --- osu.Game.Tournament/TournamentGame.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 608fc5f04a..6d597d5e7d 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -2,15 +2,25 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Game.Graphics; using osu.Game.Graphics.Cursor; +using osu.Game.Tournament.Models; using osuTK.Graphics; namespace osu.Game.Tournament { public class TournamentGame : TournamentGameBase { - public static readonly Color4 COLOUR_RED = new Color4(144, 0, 0, 255); - public static readonly Color4 COLOUR_BLUE = new Color4(0, 84, 144, 255); + public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; + + public static readonly Color4 COLOUR_RED = OsuColour.FromHex("#AA1414"); + public static readonly Color4 COLOUR_BLUE = OsuColour.FromHex("#1462AA"); + + public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = OsuColour.FromHex("#fff"); + public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = OsuColour.FromHex("#000"); + + public static readonly Color4 TEXT_COLOUR = OsuColour.FromHex("#fff"); protected override void LoadComplete() { From 129c8fe24ff86054d5875650d47acf5da098307d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:03:34 +0900 Subject: [PATCH 126/387] Add helper method to get winning team colour --- osu.Game.Tournament/Models/TournamentMatch.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs index 06cce3d59e..8ebcbf4e15 100644 --- a/osu.Game.Tournament/Models/TournamentMatch.cs +++ b/osu.Game.Tournament/Models/TournamentMatch.cs @@ -90,6 +90,8 @@ namespace osu.Game.Tournament.Models [JsonIgnore] public TournamentTeam Loser => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team2.Value : Team1.Value; + public TeamColour WinnerColour => Winner == Team1.Value ? TeamColour.Red : TeamColour.Blue; + public int PointsToWin => Round.Value?.BestOf.Value / 2 + 1 ?? 0; /// From aeb6bf5b4619e139526600b5f2fd78ca5f72bfd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:36:07 +0900 Subject: [PATCH 127/387] Remove unnecessary width specification on editor screens --- osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index e4256e727d..8e5df72cc8 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -40,7 +40,6 @@ namespace osu.Game.Tournament.Screens.Editors new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - Width = 0.9f, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Child = flow = new FillFlowContainer From 01e32896eebdbdaf880610b888993ce9f061e81e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 17:43:47 +0900 Subject: [PATCH 128/387] Make save changes button more prominent --- osu.Game.Tournament/TournamentGameBase.cs | 37 +++++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 435f315c8d..41165ca141 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -22,6 +22,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osu.Game.Users; +using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -74,16 +75,40 @@ namespace osu.Game.Tournament AddRange(new[] { - new TourneyButton + new Container { - Text = "Save Changes", - Width = 140, - Height = 50, + CornerRadius = 10, Depth = float.MinValue, + Position = new Vector2(5), + Masking = true, + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Padding = new MarginPadding(10), - Action = SaveChanges, + Children = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.2f), + RelativeSizeAxes = Axes.Both, + }, + new TourneyButton + { + Text = "Save Changes", + Width = 140, + Height = 50, + Padding = new MarginPadding + { + Top = 10, + Left = 10, + }, + Margin = new MarginPadding + { + Right = 10, + Bottom = 10, + }, + Action = SaveChanges, + }, + } }, heightWarning = new Container { From aed52179f0ffaea6be4e15775857370201ee5fd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 16:29:11 +0900 Subject: [PATCH 129/387] Fix weird reverse logic --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 6 +++--- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index d632e7c5f3..6ba57c60b8 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -162,7 +162,7 @@ namespace osu.Game.Tournament.Screens.Gameplay void expand() { - chat?.Expand(); + chat?.Contract(); using (BeginDelayedSequence(300, true)) { @@ -176,7 +176,7 @@ namespace osu.Game.Tournament.Screens.Gameplay SongBar.Expanded = false; scoreDisplay.FadeOut(100); using (chat?.BeginDelayedSequence(500)) - chat?.Contract(); + chat?.Expand(); } switch (state.NewValue) @@ -203,7 +203,7 @@ namespace osu.Game.Tournament.Screens.Gameplay break; default: - chat.Expand(); + chat.Contract(); expand(); break; } diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 21d0bcc4bf..881dd19d8e 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -92,13 +92,13 @@ namespace osu.Game.Online.Chat textbox.Text = string.Empty; } - public void Contract() + public void Expand() { this.FadeIn(300); this.MoveToY(0, 500, Easing.OutQuint); } - public void Expand() + public void Contract() { this.FadeOut(200); this.MoveToY(100, 500, Easing.In); From 1c5d6e0cf453eea4323ac204150b0811cd770057 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:07:54 +0900 Subject: [PATCH 130/387] Split out nested classes to higher level for better code sharing --- .../TestSceneDrawableTournamentTeam.cs | 124 ++++++++++ .../Components/DrawableTeamFlag.cs | 33 +++ .../Components/DrawableTeamHeader.cs | 20 ++ .../Components/DrawableTeamTitle.cs | 32 +++ .../Components/DrawableTeamTitleWithHeader.cs | 30 +++ .../Components/DrawableTeamWithPlayers.cs | 71 ++++++ .../Components/DrawableTournamentTeam.cs | 6 +- .../Components/DrawableTournamentTitleText.cs | 16 ++ .../Components/RoundDisplay.cs | 36 +++ .../TournamentSpriteTextWithBackground.cs | 37 +++ .../Screens/Drawings/Components/Group.cs | 49 ---- .../Screens/Drawings/Components/GroupTeam.cs | 60 +++++ .../Screens/Editors/TeamEditorScreen.cs | 13 - .../Gameplay/Components/MatchHeader.cs | 228 +++++------------- .../Gameplay/Components/RoundDisplay.cs | 56 +++++ .../Gameplay/Components/TeamDisplay.cs | 50 ++++ .../Screens/Gameplay/Components/TeamScore.cs | 38 +++ 17 files changed, 662 insertions(+), 237 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamFlag.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamHeader.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamTitle.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs create mode 100644 osu.Game.Tournament/Components/DrawableTournamentTitleText.cs create mode 100644 osu.Game.Tournament/Components/RoundDisplay.cs create mode 100644 osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs create mode 100644 osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs create mode 100644 osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs create mode 100644 osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs create mode 100644 osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs new file mode 100644 index 0000000000..41f7d3d847 --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -0,0 +1,124 @@ +// 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 osu.Framework.Graphics; +using osu.Game.Tests.Visual; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Drawings.Components; +using osu.Game.Tournament.Screens.Gameplay.Components; +using osu.Game.Tournament.Screens.Ladder.Components; +using osu.Game.Users; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneDrawableTournamentTeam : OsuGridTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableTeamFlag), + typeof(DrawableTeamTitle), + typeof(DrawableTeamTitleWithHeader), + typeof(DrawableMatchTeam), + typeof(DrawableTeamWithPlayers), + typeof(GroupTeam), + typeof(TeamDisplay), + }; + + public TestSceneDrawableTournamentTeam() + : base(4, 3) + { + var team = new TournamentTeam + { + FlagName = { Value = "AU" }, + FullName = { Value = "Australia" }, + Players = + { + new User { Username = "ASecretBox" }, + new User { Username = "Dereban" }, + new User { Username = "mReKk" }, + new User { Username = "uyghti" }, + new User { Username = "Parkes" }, + new User { Username = "Shiroha" }, + new User { Username = "Jordan The Bear" }, + } + }; + + var match = new TournamentMatch { Team1 = { Value = team } }; + + int i = 0; + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "DrawableTeamFlag" }, + new DrawableTeamFlag(team) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "DrawableTeamTitle" }, + new DrawableTeamTitle(team) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "DrawableTeamTitleWithHeader" }, + new DrawableTeamTitleWithHeader(team, TeamColour.Red) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "DrawableMatchTeam" }, + new DrawableMatchTeam(team, match, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "TeamWithPlayers" }, + new DrawableTeamWithPlayers(team, TeamColour.Blue) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "GroupTeam" }, + new GroupTeam(team) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "TeamDisplay" }, + new TeamDisplay(team, TournamentGame.COLOUR_RED, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs new file mode 100644 index 0000000000..8c85c9a46f --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamFlag : Sprite + { + private readonly TournamentTeam team; + + [UsedImplicitly] + private Bindable flag; + + public DrawableTeamFlag(TournamentTeam team) + { + this.team = team; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + if (team == null) return; + + (flag = team.FlagName.GetBoundCopy()).BindValueChanged(acronym => Texture = textures.Get($@"Flags/{team.FlagName}"), true); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamHeader.cs b/osu.Game.Tournament/Components/DrawableTeamHeader.cs new file mode 100644 index 0000000000..3d9e8a6e00 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamHeader.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Tournament.Models; +using osuTK; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamHeader : TournamentSpriteTextWithBackground + { + public DrawableTeamHeader(TeamColour colour) + { + Background.Colour = TournamentGame.GetTeamColour(colour); + + Text.Colour = TournamentGame.TEXT_COLOUR; + Text.Text = $"Team {colour}".ToUpperInvariant(); + Text.Scale = new Vector2(0.6f); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamTitle.cs b/osu.Game.Tournament/Components/DrawableTeamTitle.cs new file mode 100644 index 0000000000..5aac37259f --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamTitle.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Textures; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamTitle : TournamentSpriteTextWithBackground + { + private readonly TournamentTeam team; + + [UsedImplicitly] + private Bindable acronym; + + public DrawableTeamTitle(TournamentTeam team) + { + this.team = team; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + if (team == null) return; + + (acronym = team.Acronym.GetBoundCopy()).BindValueChanged(acronym => Text.Text = team?.FullName.Value ?? string.Empty, true); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs new file mode 100644 index 0000000000..ceffe3d315 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -0,0 +1,30 @@ +// 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.Game.Tournament.Models; +using osuTK; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamTitleWithHeader : CompositeDrawable + { + public DrawableTeamTitleWithHeader(TournamentTeam team, TeamColour colour) + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new DrawableTeamHeader(colour), + new DrawableTeamTitle(team), + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs new file mode 100644 index 0000000000..0b80bef903 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs @@ -0,0 +1,71 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Tournament.Models; +using osu.Game.Users; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamWithPlayers : CompositeDrawable + { + public DrawableTeamWithPlayers(TournamentTeam team, TeamColour colour) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(30), + Children = new Drawable[] + { + new DrawableTeamTitleWithHeader(team, colour), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 10 }, + Spacing = new Vector2(30), + Children = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + ChildrenEnumerable = team?.Players.Select(createPlayerText).Take(5) ?? Enumerable.Empty() + }, + new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + ChildrenEnumerable = team?.Players.Select(p => new TournamentSpriteText + { + Text = p.Username, + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), + Colour = Color4.White, + }).Skip(5) ?? Enumerable.Empty() + }, + } + }, + } + }, + }; + + TournamentSpriteText createPlayerText(User p) => + new TournamentSpriteText + { + Text = p.Username, + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), + Colour = Color4.White, + }; + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs index 99116d4a17..f8aed26ce1 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs @@ -23,14 +23,11 @@ namespace osu.Game.Tournament.Components [UsedImplicitly] private Bindable acronym; - [UsedImplicitly] - private Bindable flag; - protected DrawableTournamentTeam(TournamentTeam team) { Team = team; - Flag = new Sprite + Flag = new DrawableTeamFlag(team) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit @@ -48,7 +45,6 @@ namespace osu.Game.Tournament.Components if (Team == null) return; (acronym = Team.Acronym.GetBoundCopy()).BindValueChanged(acronym => AcronymText.Text = Team?.Acronym.Value?.ToUpperInvariant() ?? string.Empty, true); - (flag = Team.FlagName.GetBoundCopy()).BindValueChanged(acronym => Flag.Texture = textures.Get($@"Flags/{Team.FlagName}"), true); } } } diff --git a/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs b/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs new file mode 100644 index 0000000000..4fbc6cd060 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTournamentTitleText : TournamentSpriteText + { + public DrawableTournamentTitleText() + { + Text = "osu!taiko world cup 2020"; + Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold); + } + } +} diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs new file mode 100644 index 0000000000..dd56c83c57 --- /dev/null +++ b/osu.Game.Tournament/Components/RoundDisplay.cs @@ -0,0 +1,36 @@ +// 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.Game.Graphics; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public class RoundDisplay : CompositeDrawable + { + public RoundDisplay(TournamentMatch match) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DrawableTournamentTitleText(), + new TournamentSpriteText + { + Text = match.Round.Value?.Name.Value ?? "Unknown Round", + Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold) + }, + } + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs new file mode 100644 index 0000000000..d92b9eb605 --- /dev/null +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs @@ -0,0 +1,37 @@ +// 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; + +namespace osu.Game.Tournament.Components +{ + public class TournamentSpriteTextWithBackground : CompositeDrawable + { + protected readonly TournamentSpriteText Text; + protected readonly Box Background; + + public TournamentSpriteTextWithBackground(string text = "") + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + Background = new Box + { + Colour = TournamentGame.ELEMENT_BACKGROUND_COLOUR, + RelativeSizeAxes = Axes.Both, + }, + Text = new TournamentSpriteText + { + Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR, + Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50), + Padding = new MarginPadding { Left = 10, Right = 20 }, + Text = text + } + }; + } + } +} diff --git a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs index 4126f2db65..ece1c431e2 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -116,53 +115,5 @@ namespace osu.Game.Tournament.Screens.Drawings.Components sb.AppendLine(gt.Team.FullName.Value); return sb.ToString(); } - - private class GroupTeam : DrawableTournamentTeam - { - private readonly FillFlowContainer innerContainer; - - public GroupTeam(TournamentTeam team) - : base(team) - { - Width = 36; - AutoSizeAxes = Axes.Y; - - Flag.Anchor = Anchor.TopCentre; - Flag.Origin = Anchor.TopCentre; - - AcronymText.Anchor = Anchor.TopCentre; - AcronymText.Origin = Anchor.TopCentre; - AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); - AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); - - InternalChildren = new Drawable[] - { - innerContainer = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5f), - - Children = new Drawable[] - { - Flag, - AcronymText - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - innerContainer.ScaleTo(1.5f); - innerContainer.ScaleTo(1f, 200); - } - } } } diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs new file mode 100644 index 0000000000..4f0ce0bbe7 --- /dev/null +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs @@ -0,0 +1,60 @@ +// 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.Game.Graphics; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; +using osuTK; + +namespace osu.Game.Tournament.Screens.Drawings.Components +{ + public class GroupTeam : DrawableTournamentTeam + { + private readonly FillFlowContainer innerContainer; + + public GroupTeam(TournamentTeam team) + : base(team) + { + Width = 36; + AutoSizeAxes = Axes.Y; + + Flag.Anchor = Anchor.TopCentre; + Flag.Origin = Anchor.TopCentre; + + AcronymText.Anchor = Anchor.TopCentre; + AcronymText.Origin = Anchor.TopCentre; + AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); + AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); + + InternalChildren = new Drawable[] + { + innerContainer = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5f), + + Children = new Drawable[] + { + Flag, + AcronymText + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + innerContainer.ScaleTo(1.5f); + innerContainer.ScaleTo(1f, 200); + } + } +} diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 631393c6f4..7468c9484d 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -172,19 +172,6 @@ namespace osu.Game.Tournament.Screens.Editors drawableContainer.Child = new DrawableTeamFlag(Model); } - private class DrawableTeamFlag : DrawableTournamentTeam - { - public DrawableTeamFlag(TournamentTeam team) - : base(team) - { - InternalChild = Flag; - RelativeSizeAxes = Axes.Both; - - Flag.Anchor = Anchor.Centre; - Flag.Origin = Anchor.Centre; - } - } - public class PlayerEditor : CompositeDrawable { private readonly TournamentTeam team; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index ce17c392d0..c86132a802 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -5,15 +5,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Showcase; -using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Tournament.Screens.Gameplay.Components @@ -46,181 +40,75 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components }, }; } + } - private class TeamScoreDisplay : CompositeDrawable + public class TeamScoreDisplay : CompositeDrawable + { + private readonly TeamColour teamColour; + + private readonly Bindable currentMatch = new Bindable(); + private readonly Bindable currentTeam = new Bindable(); + private readonly Bindable currentTeamScore = new Bindable(); + + public TeamScoreDisplay(TeamColour teamColour) { - private readonly TeamColour teamColour; + this.teamColour = teamColour; - private readonly Bindable currentMatch = new Bindable(); - private readonly Bindable currentTeam = new Bindable(); - private readonly Bindable currentTeamScore = new Bindable(); + RelativeSizeAxes = Axes.Y; + Width = 300; + } - public TeamScoreDisplay(TeamColour teamColour) + [BackgroundDependencyLoader] + private void load(LadderInfo ladder) + { + currentMatch.BindValueChanged(matchChanged); + currentMatch.BindTo(ladder.CurrentMatch); + } + + private void matchChanged(ValueChangedEvent match) + { + currentTeamScore.UnbindBindings(); + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); + + currentTeam.UnbindBindings(); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + + // team may change to same team, which means score is not in a good state. + // thus we handle this manually. + teamChanged(currentTeam.Value); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + switch (e.Button) { - this.teamColour = teamColour; + case MouseButton.Left: + if (currentTeamScore.Value < currentMatch.Value.PointsToWin) + currentTeamScore.Value++; + return true; - RelativeSizeAxes = Axes.Y; - Width = 300; + case MouseButton.Right: + if (currentTeamScore.Value > 0) + currentTeamScore.Value--; + return true; } - [BackgroundDependencyLoader] - private void load(LadderInfo ladder) + return base.OnMouseDown(e); + } + + private void teamChanged(TournamentTeam team) + { + var colour = teamColour == TeamColour.Red ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; + var flip = teamColour == TeamColour.Red; + + InternalChildren = new Drawable[] { - currentMatch.BindValueChanged(matchChanged); - currentMatch.BindTo(ladder.CurrentMatch); - } - - private void matchChanged(ValueChangedEvent match) - { - currentTeamScore.UnbindBindings(); - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - - currentTeam.UnbindBindings(); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); - - // team may change to same team, which means score is not in a good state. - // thus we handle this manually. - teamChanged(currentTeam.Value); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - switch (e.Button) + new TeamDisplay(team, colour, flip), + new TeamScore(currentTeamScore, flip, currentMatch.Value.PointsToWin) { - case MouseButton.Left: - if (currentTeamScore.Value < currentMatch.Value.PointsToWin) - currentTeamScore.Value++; - return true; - - case MouseButton.Right: - if (currentTeamScore.Value > 0) - currentTeamScore.Value--; - return true; + Colour = colour } - - return base.OnMouseDown(e); - } - - private void teamChanged(TournamentTeam team) - { - var colour = teamColour == TeamColour.Red ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; - var flip = teamColour != TeamColour.Red; - - InternalChildren = new Drawable[] - { - new TeamDisplay(team, colour, flip), - new TeamScore(currentTeamScore, flip, currentMatch.Value.PointsToWin) - { - Colour = colour - } - }; - } - } - - private class TeamScore : CompositeDrawable - { - private readonly Bindable currentTeamScore = new Bindable(); - private readonly StarCounter counter; - - public TeamScore(Bindable score, bool flip, int count) - { - var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; - - Anchor = anchor; - Origin = anchor; - - InternalChild = counter = new StarCounter(count) - { - Anchor = anchor, - X = (flip ? -1 : 1) * 90, - Y = 5, - Scale = flip ? new Vector2(-1, 1) : Vector2.One, - }; - - currentTeamScore.BindValueChanged(scoreChanged); - currentTeamScore.BindTo(score); - } - - private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; - } - - private class TeamDisplay : DrawableTournamentTeam - { - public TeamDisplay(TournamentTeam team, Color4 colour, bool flip) - : base(team) - { - RelativeSizeAxes = Axes.Both; - - var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; - - Anchor = Origin = anchor; - - Flag.Anchor = Flag.Origin = anchor; - Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(60, 40); - Flag.Margin = new MarginPadding(20); - - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - Flag, - new TournamentSpriteText - { - Text = team?.FullName.Value.ToUpper() ?? "???", - X = (flip ? -1 : 1) * 90, - Y = -10, - Colour = colour, - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 20), - Origin = anchor, - Anchor = anchor, - }, - } - }; - } - } - - private class RoundDisplay : CompositeDrawable - { - private readonly Bindable currentMatch = new Bindable(); - - private readonly TournamentSpriteText text; - - public RoundDisplay() - { - Width = 200; - Height = 20; - - Masking = true; - CornerRadius = 10; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.18f), - RelativeSizeAxes = Axes.Both, - }, - text = new TournamentSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 16), - }, - }; - } - - [BackgroundDependencyLoader] - private void load(LadderInfo ladder) - { - currentMatch.BindValueChanged(matchChanged); - currentMatch.BindTo(ladder.CurrentMatch); - } - - private void matchChanged(ValueChangedEvent match) => - text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; + }; } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs new file mode 100644 index 0000000000..5322cf9a76 --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs @@ -0,0 +1,56 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Tournament.Models; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class RoundDisplay : CompositeDrawable + { + private readonly Bindable currentMatch = new Bindable(); + + private readonly TournamentSpriteText text; + + public RoundDisplay() + { + Width = 200; + Height = 20; + + Masking = true; + CornerRadius = 10; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.18f), + RelativeSizeAxes = Axes.Both, + }, + text = new TournamentSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 16), + }, + }; + } + + [BackgroundDependencyLoader] + private void load(LadderInfo ladder) + { + currentMatch.BindValueChanged(matchChanged); + currentMatch.BindTo(ladder.CurrentMatch); + } + + private void matchChanged(ValueChangedEvent match) => + text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; + } +} diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs new file mode 100644 index 0000000000..891435c48e --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -0,0 +1,50 @@ +// 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.Game.Graphics; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class TeamDisplay : DrawableTournamentTeam + { + public TeamDisplay(TournamentTeam team, Color4 colour, bool flip) + : base(team) + { + RelativeSizeAxes = Axes.Both; + + var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; + + Anchor = Origin = anchor; + + Flag.Anchor = Flag.Origin = anchor; + Flag.RelativeSizeAxes = Axes.None; + Flag.Size = new Vector2(60, 40); + Flag.Margin = new MarginPadding(20); + + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + Flag, + new TournamentSpriteText + { + Text = team?.FullName.Value.ToUpper() ?? "???", + X = (flip ? -1 : 1) * 90, + Y = -10, + Colour = colour, + Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 20), + Origin = anchor, + Anchor = anchor, + }, + } + }; + } + } +} diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs new file mode 100644 index 0000000000..608d98a16a --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -0,0 +1,38 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class TeamScore : CompositeDrawable + { + private readonly Bindable currentTeamScore = new Bindable(); + private readonly StarCounter counter; + + public TeamScore(Bindable score, bool flip, int count) + { + var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; + + Anchor = anchor; + Origin = anchor; + + InternalChild = counter = new StarCounter(count) + { + Anchor = anchor, + X = (flip ? -1 : 1) * 90, + Y = 5, + Scale = flip ? new Vector2(-1, 1) : Vector2.One, + }; + + currentTeamScore.BindValueChanged(scoreChanged); + currentTeamScore.BindTo(score); + } + + private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; + } +} From 4d74493289a7de37137727545b67629ca3576343 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:08:05 +0900 Subject: [PATCH 131/387] Initial pass of win screen design update --- .../Screens/TeamWin/TeamWinScreen.cs | 110 ++++-------------- 1 file changed, 23 insertions(+), 87 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 1765ab7ba2..8b3f4488d0 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -10,7 +10,6 @@ using osu.Game.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.TeamWin { @@ -73,105 +72,42 @@ namespace osu.Game.Tournament.Screens.TeamWin return; } - bool redWin = match.Winner == match.Team1.Value; - redWinVideo.Alpha = redWin ? 1 : 0; - blueWinVideo.Alpha = redWin ? 0 : 1; + redWinVideo.Alpha = match.WinnerColour == TeamColour.Red ? 1 : 0; + blueWinVideo.Alpha = match.WinnerColour == TeamColour.Blue ? 1 : 0; mainContainer.Children = new Drawable[] { - new TeamFlagDisplay(match.Winner) + new DrawableTeamFlag(match.Winner) { Size = new Vector2(300, 200), Scale = new Vector2(0.5f), Anchor = Anchor.Centre, Origin = Anchor.Centre, - X = -387, + Position = new Vector2(-300, 10), }, - new TournamentSpriteText + new FillFlowContainer { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, Anchor = Anchor.Centre, - Origin = Anchor.TopLeft, - Position = new Vector2(78, -70), - Colour = OsuColour.Gray(0.33f), - Text = match.Round.Value?.Name.Value ?? "Unknown Round", - Font = OsuFont.Torus.With(size: 30, weight: FontWeight.Regular) - }, - new TeamWithPlayers(match.Winner, redWin) - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Height = 0.6f, - Anchor = Anchor.Centre, - Origin = Anchor.TopLeft, - Position = new Vector2(78, 0), + Origin = Anchor.Centre, + X = 260, + Children = new Drawable[] + { + new RoundDisplay(match) + { + Margin = new MarginPadding { Bottom = 30 }, + }, + new TournamentSpriteText + { + Text = "WINNER", + Font = OsuFont.Torus.With(size: 100, weight: FontWeight.Bold), + Margin = new MarginPadding { Bottom = 50 }, + }, + new DrawableTeamWithPlayers(match.Winner, match.WinnerColour) + } }, }; } - - private class TeamWithPlayers : CompositeDrawable - { - public TeamWithPlayers(TournamentTeam team, bool left = false) - { - FillFlowContainer players; - - var colour = left ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new TournamentSpriteText - { - Text = "WINNER", - Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), - Colour = Color4.Black, - }, - new TournamentSpriteText - { - Text = team?.FullName.Value ?? "???", - Font = OsuFont.Torus.With(size: 30, weight: FontWeight.SemiBold), - Colour = Color4.Black, - }, - players = new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 10 }, - }, - } - }, - }; - - if (team != null) - { - foreach (var p in team.Players) - { - players.Add(new TournamentSpriteText - { - Text = p.Username, - Font = OsuFont.Torus.With(size: 24), - Colour = colour, - Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, - }); - } - } - } - } - - private class TeamFlagDisplay : DrawableTournamentTeam - { - public TeamFlagDisplay(TournamentTeam team) - : base(team) - { - InternalChildren = new Drawable[] - { - Flag - }; - } - } } } From 77c94afcf182ff33a893ed3d3930f9cb04e3b344 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 14:28:10 +0900 Subject: [PATCH 132/387] Add better flow logic to map pool layout when few beatmaps are present --- .../Screens/MapPool/MapPoolScreen.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index c42d0a6da3..d7aeac02cb 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -47,9 +47,10 @@ namespace osu.Game.Tournament.Screens.MapPool { Y = 100, Spacing = new Vector2(10, 10), - Padding = new MarginPadding(25), + Padding = new MarginPadding(5) { Horizontal = 100 }, Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, }, new ControlPanel { @@ -90,6 +91,7 @@ namespace osu.Game.Tournament.Screens.MapPool Text = "Reset", Action = reset }, + new ControlPanel.Spacer(), } } }; @@ -206,11 +208,15 @@ namespace osu.Game.Tournament.Screens.MapPool { mapFlows.Clear(); + int totalRows = 0; + if (match.NewValue.Round.Value != null) { FillFlowContainer currentFlow = null; string currentMod = null; + int flowCount = 0; + foreach (var b in match.NewValue.Round.Value.Beatmaps) { if (currentFlow == null || currentMod != b.Mods) @@ -224,6 +230,15 @@ namespace osu.Game.Tournament.Screens.MapPool }); currentMod = b.Mods; + + totalRows++; + flowCount = 0; + } + + if (++flowCount > 2) + { + totalRows++; + flowCount = 0; } currentFlow.Add(new TournamentBeatmapPanel(b.BeatmapInfo, b.Mods) @@ -233,6 +248,10 @@ namespace osu.Game.Tournament.Screens.MapPool }); } } + + if (totalRows > 8) + // remove horizontal padding to increase flow width to 3 panels + mapFlows.Padding = new MarginPadding(5); } } } From 29817304423d154a39a0f5212e9ac2aeb3514148 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 13:51:11 +0900 Subject: [PATCH 133/387] Initial pass of map pool screen design update --- .../Components/TournamentBeatmapPanel.cs | 22 ++----------------- .../Screens/MapPool/MapPoolScreen.cs | 5 +++++ 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 394ffe304e..09c4a96807 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tournament.Components private readonly string mods; private const float horizontal_padding = 10; - private const float vertical_padding = 5; + private const float vertical_padding = 10; public const float HEIGHT = 50; @@ -50,8 +50,6 @@ namespace osu.Game.Tournament.Components currentMatch.BindValueChanged(matchChanged); currentMatch.BindTo(ladder.CurrentMatch); - CornerRadius = HEIGHT / 2; - CornerExponent = 2; Masking = true; AddRangeInternal(new Drawable[] @@ -70,16 +68,12 @@ namespace osu.Game.Tournament.Components new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, Padding = new MarginPadding(vertical_padding), Direction = FillDirection.Vertical, Children = new Drawable[] { new TournamentSpriteText { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, Text = new LocalisedString(( $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")), @@ -88,9 +82,6 @@ namespace osu.Game.Tournament.Components new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding(vertical_padding), Direction = FillDirection.Horizontal, Children = new Drawable[] { @@ -170,16 +161,7 @@ namespace osu.Game.Tournament.Components BorderThickness = 6; - switch (found.Team) - { - case TeamColour.Red: - BorderColour = Color4.Red; - break; - - case TeamColour.Blue: - BorderColour = Color4.Blue; - break; - } + BorderColour = TournamentGame.GetTeamColour(found.Team); switch (found.Type) { diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index c42d0a6da3..4f3f7cfdbf 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -42,6 +42,11 @@ namespace osu.Game.Tournament.Screens.MapPool { InternalChildren = new Drawable[] { + new TourneyVideo("gameplay") + { + Loop = true, + RelativeSizeAxes = Axes.Both, + }, new MatchHeader(), mapFlows = new FillFlowContainer> { From ba6c4abbe6568c3d4acc9701dac310feae4ce1a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 01:37:15 +0900 Subject: [PATCH 134/387] Initial pass of ladder screen design update --- .../Ladder/Components/DrawableMatchTeam.cs | 60 +++++++++---------- .../Components/DrawableTournamentMatch.cs | 11 ++-- .../Components/DrawableTournamentRound.cs | 5 +- .../Screens/Ladder/LadderScreen.cs | 10 +++- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 88d7b95b0c..4f10a7e7d0 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -27,12 +27,12 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private readonly bool losers; private TournamentSpriteText scoreText; private Box background; + private Box backgroundRight; private readonly Bindable score = new Bindable(); private readonly BindableBool completed = new BindableBool(); private Color4 colourWinner; - private Color4 colourNormal; private readonly Func isWinner; private LadderEditorScreen ladderEditor; @@ -60,15 +60,12 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.losers = losers; Size = new Vector2(150, 40); - Masking = true; - CornerRadius = 5; - Flag.Scale = new Vector2(0.9f); Flag.Anchor = Flag.Origin = Anchor.CentreLeft; AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; AcronymText.Padding = new MarginPadding { Left = 50 }; - AcronymText.Font = OsuFont.Torus.With(size: 24); + AcronymText.Font = OsuFont.Torus.With(size: 22, weight: FontWeight.Bold); if (match != null) { @@ -85,8 +82,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { this.ladderEditor = ladderEditor; - colourWinner = losers ? colours.YellowDarker : colours.BlueDarker; - colourNormal = OsuColour.Gray(0.2f); + colourWinner = losers + ? OsuColour.FromHex("#8E7F48") + : OsuColour.FromHex("#1462AA"); InternalChildren = new Drawable[] { @@ -102,29 +100,28 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { AcronymText, Flag, - new Container + } + }, + new Container + { + Masking = true, + Width = 0.3f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundRight = new Box { - Masking = true, - CornerRadius = 5, - Width = 0.3f, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, + Colour = OsuColour.Gray(0.1f), + Alpha = 0.8f, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.1f), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - scoreText = new TournamentSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 20), - } - } + }, + scoreText = new TournamentSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 22), } } } @@ -181,9 +178,12 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { bool winner = completed.Value && isWinner?.Invoke() == true; - background.FadeColour(winner ? colourWinner : colourNormal, winner ? 500 : 0, Easing.OutQuint); + background.FadeColour(winner ? Color4.White : OsuColour.FromHex("#444"), winner ? 500 : 0, Easing.OutQuint); + backgroundRight.FadeColour(winner ? colourWinner : OsuColour.FromHex("#333"), winner ? 500 : 0, Easing.OutQuint); - scoreText.Font = AcronymText.Font = OsuFont.Torus.With(weight: winner ? FontWeight.Bold : FontWeight.Regular); + AcronymText.Colour = winner ? Color4.Black : Color4.White; + + scoreText.Font = scoreText.Font.With(weight: winner ? FontWeight.Bold : FontWeight.Regular); } public MenuItem[] ContextMenuItems diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index c4b670f059..82a4c7017f 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -45,9 +46,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { selectionBox = new Container { - CornerRadius = 5, - Masking = true, - Scale = new Vector2(1.05f), + Scale = new Vector2(1.1f), RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -57,14 +56,12 @@ namespace osu.Game.Tournament.Screens.Ladder.Components }, currentMatchSelectionBox = new Container { - CornerRadius = 5, - Masking = true, - Scale = new Vector2(1.05f), + Scale = new Vector2(1.1f), RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, - Colour = Color4.OrangeRed, + Colour = OsuColour.FromHex("#D24747"), Child = new Box { RelativeSizeAxes = Axes.Both } }, Flow = new FillFlowContainer diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs index d14ebb4d03..cad0b827c0 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Tournament.Models; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Ladder.Components { @@ -33,14 +32,14 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { textDescription = new TournamentSpriteText { - Colour = Color4.Black, + Colour = TournamentGame.TEXT_COLOUR, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre }, textName = new TournamentSpriteText { Font = OsuFont.Torus.With(weight: FontWeight.Bold), - Colour = Color4.Black, + Colour = TournamentGame.TEXT_COLOUR, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre }, diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 7b265ded32..c7e59cfa7b 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -32,8 +32,8 @@ namespace osu.Game.Tournament.Screens.Ladder [BackgroundDependencyLoader] private void load(OsuColour colours, Storage storage) { - normalPathColour = colours.BlueDarker.Darken(2); - losersPathColour = colours.YellowDarker.Darken(2); + normalPathColour = OsuColour.FromHex("#66D1FF"); + losersPathColour = OsuColour.FromHex("#FFC700"); RelativeSizeAxes = Axes.Both; @@ -47,6 +47,12 @@ namespace osu.Game.Tournament.Screens.Ladder RelativeSizeAxes = Axes.Both, Loop = true, }, + new DrawableTournamentTitleText + { + Y = 100, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, ScrollContent = new LadderDragContainer { RelativeSizeAxes = Axes.Both, From 66e54ba9837f4c8bf5aa7fdc8788c8dc7c6e7eb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 12:49:09 +0900 Subject: [PATCH 135/387] Increase flexibility of StarCounter component --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 12 +-- .../Graphics/UserInterface/StarCounter.cs | 101 ++++++++++-------- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index ffd6f55b53..88bb83b446 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -160), - CountStars = 5, + Current = 5, }; Add(stars); @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -190), - Text = stars.CountStars.ToString("0.00"), + Text = stars.Current.ToString("0.00"), }; Add(starsLabel); @@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Gameplay comboCounter.Current.Value = 0; numerator = denominator = 0; accuracyCounter.SetFraction(0, 0); - stars.CountStars = 0; - starsLabel.Text = stars.CountStars.ToString("0.00"); + stars.Current = 0; + starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Hit! :D", delegate @@ -91,8 +91,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep(@"Alter stars", delegate { - stars.CountStars = RNG.NextSingle() * (stars.StarCount + 1); - starsLabel.Text = stars.CountStars.ToString("0.00"); + stars.Current = RNG.NextSingle() * (stars.StarCount + 1); + starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Stop counters", delegate diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index 586cd2ce84..b13d6485ac 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -13,7 +13,7 @@ namespace osu.Game.Graphics.UserInterface { public class StarCounter : Container { - private readonly Container stars; + private readonly FillFlowContainer stars; /// /// Maximum amount of stars displayed. @@ -23,34 +23,29 @@ namespace osu.Game.Graphics.UserInterface /// public int StarCount { get; } - private double animationDelay => 80; + /// + /// The added delay for each subsequent star to be animated. + /// + protected virtual double AnimationDelay => 80; - private double scalingDuration => 1000; - private Easing scalingEasing => Easing.OutElasticHalf; - private float minStarScale => 0.4f; - - private double fadingDuration => 100; - private float minStarAlpha => 0.5f; - - private const float star_size = 20; private const float star_spacing = 4; - private float countStars; + private float current; /// /// Amount of stars represented. /// - public float CountStars + public float Current { - get => countStars; + get => current; set { - if (countStars == value) return; + if (current == value) return; if (IsLoaded) - transformCount(value); - countStars = value; + animate(value); + current = value; } } @@ -71,11 +66,13 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(star_spacing), - ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => new Star { Alpha = minStarAlpha }) + ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => CreateStar()) } }; } + public virtual Star CreateStar() => new DefaultStar(); + protected override void LoadComplete() { base.LoadComplete(); @@ -86,63 +83,60 @@ namespace osu.Game.Graphics.UserInterface public void ResetCount() { - countStars = 0; + current = 0; StopAnimation(); } public void ReplayAnimation() { - var t = countStars; + var t = current; ResetCount(); - CountStars = t; + Current = t; } public void StopAnimation() { - int i = 0; - + animate(current); foreach (var star in stars.Children) + star.FinishTransforms(true); + } + + private float getStarScale(int i, float value) => i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, 0, 1.0f, i, i + 1); + + private void animate(float newValue) + { + for (var i = 0; i < stars.Children.Count; i++) { + var star = stars.Children[i]; + star.ClearTransforms(true); - star.FadeTo(i < countStars ? 1.0f : minStarAlpha); - star.Icon.ScaleTo(getStarScale(i, countStars)); - i++; + + double delay = (current <= newValue ? Math.Max(i - current, 0) : Math.Max(current - 1 - i, 0)) * AnimationDelay; + + using (star.BeginDelayedSequence(delay, true)) + star.DisplayAt(getStarScale(i, newValue)); } } - private float getStarScale(int i, float value) + public class DefaultStar : Star { - if (value <= i) - return minStarScale; + private const double scaling_duration = 1000; - return i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, minStarScale, 1.0f, i, i + 1); - } + private const double fading_duration = 100; - private void transformCount(float newValue) - { - int i = 0; + private const Easing scaling_easing = Easing.OutElasticHalf; - foreach (var star in stars.Children) - { - star.ClearTransforms(true); + private const float min_star_scale = 0.4f; - var delay = (countStars <= newValue ? Math.Max(i - countStars, 0) : Math.Max(countStars - 1 - i, 0)) * animationDelay; - star.Delay(delay).FadeTo(i < newValue ? 1.0f : minStarAlpha, fadingDuration); - star.Icon.Delay(delay).ScaleTo(getStarScale(i, newValue), scalingDuration, scalingEasing); + private const float star_size = 20; - i++; - } - } - - private class Star : Container - { public readonly SpriteIcon Icon; - public Star() + public DefaultStar() { Size = new Vector2(star_size); - Child = Icon = new SpriteIcon + InternalChild = Icon = new SpriteIcon { Size = new Vector2(star_size), Icon = FontAwesome.Solid.Star, @@ -150,6 +144,19 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.Centre, }; } + + public override void DisplayAt(float scale) + { + scale = Math.Clamp(scale, min_star_scale, 1); + + this.FadeTo(scale, fading_duration); + Icon.ScaleTo(scale, scaling_duration, scaling_easing); + } + } + + public abstract class Star : CompositeDrawable + { + public abstract void DisplayAt(float scale); } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d9eeec9f85..50419a5fb9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Select.Carousel }, starCounter = new StarCounter { - CountStars = (float)beatmap.StarDifficulty, + Current = (float)beatmap.StarDifficulty, Scale = new Vector2(0.8f), } } From 86b12a384b4244bf14ae93ea19f3cd3cf13d6207 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 12:51:11 +0900 Subject: [PATCH 136/387] Initial pass of gameplay screen design update --- .../TestSceneDrawableTournamentTeam.cs | 3 +- .../Screens/TestSceneGameplayScreen.cs | 11 +++ .../Gameplay/Components/MatchHeader.cs | 39 +++++---- .../Gameplay/Components/RoundDisplay.cs | 37 +-------- .../Gameplay/Components/TeamDisplay.cs | 73 ++++++++++++----- .../Screens/Gameplay/Components/TeamScore.cs | 81 +++++++++++++++++-- .../Screens/Gameplay/GameplayScreen.cs | 32 +------- 7 files changed, 170 insertions(+), 106 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index 41f7d3d847..01edcb66e4 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; @@ -113,7 +114,7 @@ namespace osu.Game.Tournament.Tests.Components Cell(i).AddRange(new Drawable[] { new TournamentSpriteText { Text = "TeamDisplay" }, - new TeamDisplay(team, TournamentGame.COLOUR_RED, false) + new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 9de00818a5..964930a8de 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.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 osu.Framework.Allocation; using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Gameplay; +using osu.Game.Tournament.Screens.Gameplay.Components; namespace osu.Game.Tournament.Tests.Screens { @@ -12,6 +15,14 @@ namespace osu.Game.Tournament.Tests.Screens [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); + public override IReadOnlyList RequiredTypes => new[] + { + typeof(TeamScore), + typeof(TeamScoreDisplay), + typeof(TeamDisplay), + typeof(MatchHeader), + }; + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index c86132a802..45f46c462a 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -6,8 +6,9 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; -using osu.Game.Tournament.Screens.Showcase; +using osuTK; using osuTK.Input; namespace osu.Game.Tournament.Screens.Gameplay.Components @@ -21,13 +22,28 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Height = 95; Children = new Drawable[] { - new TournamentLogo(), - new RoundDisplay + new FillFlowContainer { - Y = 5, - Anchor = Anchor.BottomCentre, - Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new DrawableTournamentTitleText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.2f) + }, + new RoundDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.4f) + }, + } }, + new TeamScoreDisplay(TeamColour.Red) { Anchor = Anchor.TopLeft, @@ -55,7 +71,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components this.teamColour = teamColour; RelativeSizeAxes = Axes.Y; - Width = 300; + AutoSizeAxes = Axes.X; } [BackgroundDependencyLoader] @@ -98,16 +114,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private void teamChanged(TournamentTeam team) { - var colour = teamColour == TeamColour.Red ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; - var flip = teamColour == TeamColour.Red; - InternalChildren = new Drawable[] { - new TeamDisplay(team, colour, flip), - new TeamScore(currentTeamScore, flip, currentMatch.Value.PointsToWin) - { - Colour = colour - } + new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value.PointsToWin), }; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs index 5322cf9a76..c8b0d3bdda 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs @@ -3,46 +3,15 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; +using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay.Components { - public class RoundDisplay : CompositeDrawable + public class RoundDisplay : TournamentSpriteTextWithBackground { private readonly Bindable currentMatch = new Bindable(); - private readonly TournamentSpriteText text; - - public RoundDisplay() - { - Width = 200; - Height = 20; - - Masking = true; - CornerRadius = 10; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.18f), - RelativeSizeAxes = Axes.Both, - }, - text = new TournamentSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 16), - }, - }; - } - [BackgroundDependencyLoader] private void load(LadderInfo ladder) { @@ -51,6 +20,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } private void matchChanged(ValueChangedEvent match) => - text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; + Text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 891435c48e..4bfeb2bc81 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -1,47 +1,84 @@ // 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay.Components { public class TeamDisplay : DrawableTournamentTeam { - public TeamDisplay(TournamentTeam team, Color4 colour, bool flip) + public TeamDisplay(TournamentTeam team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) : base(team) { - RelativeSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Both; - var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; + bool flip = colour == TeamColour.Red; - Anchor = Origin = anchor; + var anchor = flip ? Anchor.TopLeft : Anchor.TopRight; - Flag.Anchor = Flag.Origin = anchor; Flag.RelativeSizeAxes = Axes.None; Flag.Size = new Vector2(60, 40); - Flag.Margin = new MarginPadding(20); + Flag.Origin = anchor; + Flag.Anchor = anchor; + + Margin = new MarginPadding(20); InternalChild = new Container { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Children = new Drawable[] { - Flag, - new TournamentSpriteText + new FillFlowContainer { - Text = team?.FullName.Value.ToUpper() ?? "???", - X = (flip ? -1 : 1) * 90, - Y = -10, - Colour = colour, - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 20), - Origin = anchor, - Anchor = anchor, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + Flag, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Origin = anchor, + Anchor = anchor, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new DrawableTeamHeader(colour) + { + Scale = new Vector2(0.75f), + Origin = anchor, + Anchor = anchor, + }, + new TeamScore(currentTeamScore, colour, pointsToWin) + { + Origin = anchor, + Anchor = anchor, + } + } + }, + new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???") + { + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + }, + } + }, + } }, } }; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index 608d98a16a..056f8387fb 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -2,10 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Tournament.Models; using osuTK; +using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay.Components { @@ -14,18 +20,16 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly Bindable currentTeamScore = new Bindable(); private readonly StarCounter counter; - public TeamScore(Bindable score, bool flip, int count) + public TeamScore(Bindable score, TeamColour colour, int count) { - var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; + bool flip = colour == TeamColour.Blue; + var anchor = flip ? Anchor.TopRight : Anchor.TopLeft; - Anchor = anchor; - Origin = anchor; + AutoSizeAxes = Axes.Both; - InternalChild = counter = new StarCounter(count) + InternalChild = counter = new TeamScoreStarCounter(count) { Anchor = anchor, - X = (flip ? -1 : 1) * 90, - Y = 5, Scale = flip ? new Vector2(-1, 1) : Vector2.One, }; @@ -33,6 +37,67 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentTeamScore.BindTo(score); } - private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; + private void scoreChanged(ValueChangedEvent score) => counter.Current = score.NewValue ?? 0; + + public class TeamScoreStarCounter : StarCounter + { + public TeamScoreStarCounter(int count) + : base(count) + { + } + + public override Star CreateStar() => new LightSquare(); + + public class LightSquare : Star + { + private Box box; + + public LightSquare() + { + Size = new Vector2(22.5f); + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderColour = OsuColour.Gray(0.5f), + BorderThickness = 3, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Transparent, + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + }, + } + }, + box = new Box + { + Colour = OsuColour.FromHex("#FFE8AD"), + RelativeSizeAxes = Axes.Both, + }, + }; + + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = OsuColour.FromHex("#FFE8AD").Opacity(0.1f), + Hollow = true, + Radius = 20, + Roundness = 10, + }; + } + + public override void DisplayAt(float scale) + { + box.FadeTo(scale, 500, Easing.OutQuint); + FadeEdgeEffectTo(0.2f * scale, 500, Easing.OutQuint); + } + } + } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 6ba57c60b8..df3ca59f1f 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -30,9 +30,6 @@ namespace osu.Game.Tournament.Screens.Gameplay private OsuButton warmupButton; private MatchIPCInfo ipc; - private readonly Color4 red = new Color4(186, 0, 18, 255); - private readonly Color4 blue = new Color4(17, 136, 170, 255); - [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } @@ -66,36 +63,11 @@ namespace osu.Game.Tournament.Screens.Gameplay // chroma key area for stable gameplay Name = "chroma", RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Height = 512, Colour = new Color4(0, 255, 0, 255), }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Y = -4, - Children = new Drawable[] - { - new Circle - { - Name = "top bar red", - RelativeSizeAxes = Axes.X, - Height = 8, - Width = 0.5f, - Colour = red, - }, - new Circle - { - Name = "top bar blue", - RelativeSizeAxes = Axes.X, - Height = 8, - Width = 0.5f, - Colour = blue, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }, - } - }, } }, scoreDisplay = new MatchScoreDisplay From e25206728f39761932ecc8379a34e33d495348be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 14:46:40 +0900 Subject: [PATCH 137/387] Hide score displays during warmup --- .../Gameplay/Components/MatchHeader.cs | 23 +++++++++++++++---- .../Gameplay/Components/TeamDisplay.cs | 6 ++++- .../Screens/Gameplay/GameplayScreen.cs | 9 ++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 45f46c462a..7e9d0178d8 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -15,6 +15,18 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public class MatchHeader : Container { + private TeamScoreDisplay teamDisplay1; + private TeamScoreDisplay teamDisplay2; + + public bool ShowScores + { + set + { + teamDisplay1.ShowScore = value; + teamDisplay2.ShowScore = value; + } + } + [BackgroundDependencyLoader] private void load() { @@ -43,13 +55,12 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components }, } }, - - new TeamScoreDisplay(TeamColour.Red) + teamDisplay1 = new TeamScoreDisplay(TeamColour.Red) { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, }, - new TeamScoreDisplay(TeamColour.Blue) + teamDisplay2 = new TeamScoreDisplay(TeamColour.Blue) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -66,6 +77,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly Bindable currentTeam = new Bindable(); private readonly Bindable currentTeamScore = new Bindable(); + private TeamDisplay teamDisplay; + + public bool ShowScore { set => teamDisplay.ShowScore = value; } + public TeamScoreDisplay(TeamColour teamColour) { this.teamColour = teamColour; @@ -116,7 +131,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { InternalChildren = new Drawable[] { - new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value.PointsToWin), + teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value.PointsToWin), }; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 4bfeb2bc81..29908e8e7c 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -12,6 +12,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public class TeamDisplay : DrawableTournamentTeam { + private readonly TeamScore score; + + public bool ShowScore { set => score.FadeTo(value ? 1 : 0, 200); } + public TeamDisplay(TournamentTeam team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) : base(team) { @@ -63,7 +67,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Origin = anchor, Anchor = anchor, }, - new TeamScore(currentTeamScore, colour, pointsToWin) + score = new TeamScore(currentTeamScore, colour, pointsToWin) { Origin = anchor, Anchor = anchor, diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index df3ca59f1f..78d27c87ff 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tournament.Screens.Gameplay Loop = true, RelativeSizeAxes = Axes.Both, }, - new MatchHeader(), + header = new MatchHeader(), new Container { RelativeSizeAxes = Axes.X, @@ -108,13 +108,18 @@ namespace osu.Game.Tournament.Screens.Gameplay currentMatch.BindTo(ladder.CurrentMatch); - warmup.BindValueChanged(w => warmupButton.Alpha = !w.NewValue ? 0.5f : 1, true); + warmup.BindValueChanged(w => + { + warmupButton.Alpha = !w.NewValue ? 0.5f : 1; + header.ShowScores = !w.NewValue; + }, true); } private ScheduledDelegate scheduledOperation; private MatchScoreDisplay scoreDisplay; private TourneyState lastState; + private MatchHeader header; private void stateChanged(ValueChangedEvent state) { From 3807c449bd350c838c9e0749c17631a63724aeb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 15:28:19 +0900 Subject: [PATCH 138/387] Update chat position --- .../Components/TournamentMatchChatDisplay.cs | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 48c5b9bd35..6963f0a0cb 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -10,7 +10,6 @@ using osu.Game.Overlays.Chat; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Components { @@ -23,11 +22,11 @@ namespace osu.Game.Tournament.Components public TournamentMatchChatDisplay() { RelativeSizeAxes = Axes.X; - Y = 100; - Size = new Vector2(0.45f, 112); - Margin = new MarginPadding(10); - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; + Size = new Vector2(0.5f, 142); + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + + CornerRadius = 0; } [BackgroundDependencyLoader(true)] @@ -75,19 +74,15 @@ namespace osu.Game.Tournament.Components { } - [BackgroundDependencyLoader] private void load(LadderInfo info) { - //if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.Id == Message.Sender.Id)) - // ColourBox.Colour = red; - //else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id)) - // ColourBox.Colour = blue; - //else if (Message.Sender.Colour != null) - // SenderText.Colour = ColourBox.Colour = OsuColour.FromHex(Message.Sender.Colour); + // if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.Id == Message.Sender.Id)) + // SenderText.Colour = TournamentGame.COLOUR_RED; + // else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id)) + // SenderText.Colour = TournamentGame.COLOUR_BLUE; + // else if (Message.Sender.Colour != null) + // SenderText.Colour = ColourBox.Colour = OsuColour.FromHex(Message.Sender.Colour); } - - private readonly Color4 red = new Color4(186, 0, 18, 255); - private readonly Color4 blue = new Color4(17, 136, 170, 255); } } } From 9bd837da4123cb2781dc25ac89ca4a2755794924 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 16:22:52 +0900 Subject: [PATCH 139/387] Update match score display --- .../Screens/TestSceneGameplayScreen.cs | 1 + .../Gameplay/Components/MatchScoreDisplay.cs | 53 ++++++++++++------- .../Screens/Gameplay/Components/TeamScore.cs | 2 +- .../Screens/Gameplay/GameplayScreen.cs | 6 +-- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 964930a8de..ae7d9d853a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tournament.Tests.Screens typeof(TeamScoreDisplay), typeof(TeamDisplay), typeof(MatchHeader), + typeof(MatchScoreDisplay), }; [BackgroundDependencyLoader] diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs index fcf1469278..ed14956793 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs @@ -11,16 +11,12 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay.Components { public class MatchScoreDisplay : CompositeDrawable { - private readonly Color4 red = new Color4(186, 0, 18, 255); - private readonly Color4 blue = new Color4(17, 136, 170, 255); - - private const float bar_height = 20; + private const float bar_height = 18; private readonly BindableInt score1 = new BindableInt(); private readonly BindableInt score2 = new BindableInt(); @@ -28,45 +24,63 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly MatchScoreCounter score1Text; private readonly MatchScoreCounter score2Text; - private readonly Circle score1Bar; - private readonly Circle score2Bar; + private readonly Drawable score1Bar; + private readonly Drawable score2Bar; public MatchScoreDisplay() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChildren = new Drawable[] + InternalChildren = new[] { - score1Bar = new Circle + new Box + { + Name = "top bar red (static)", + RelativeSizeAxes = Axes.X, + Height = bar_height / 4, + Width = 0.5f, + Colour = TournamentGame.COLOUR_RED, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight + }, + new Box + { + Name = "top bar blue (static)", + RelativeSizeAxes = Axes.X, + Height = bar_height / 4, + Width = 0.5f, + Colour = TournamentGame.COLOUR_BLUE, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft + }, + score1Bar = new Box { Name = "top bar red", RelativeSizeAxes = Axes.X, Height = bar_height, Width = 0, - Colour = red, + Colour = TournamentGame.COLOUR_RED, Anchor = Anchor.TopCentre, Origin = Anchor.TopRight }, score1Text = new MatchScoreCounter { - Colour = red, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - score2Bar = new Circle + score2Bar = new Box { Name = "top bar blue", RelativeSizeAxes = Axes.X, Height = bar_height, Width = 0, - Colour = blue, + Colour = TournamentGame.COLOUR_BLUE, Anchor = Anchor.TopCentre, Origin = Anchor.TopLeft }, score2Text = new MatchScoreCounter { - Colour = blue, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, @@ -103,10 +117,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint); } - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); - + base.UpdateAfterChildren(); score1Text.X = -Math.Max(5 + score1Text.DrawWidth / 2, score1Bar.DrawWidth); score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth); } @@ -115,7 +128,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public MatchScoreCounter() { - Margin = new MarginPadding { Top = bar_height + 5, Horizontal = 10 }; + Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; Winning = false; } @@ -123,8 +136,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components public bool Winning { set => DisplayedCountSpriteText.Font = value - ? OsuFont.Torus.With(weight: FontWeight.Regular, size: 60) - : OsuFont.Torus.With(weight: FontWeight.Light, size: 40); + ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50) + : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40); } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index 056f8387fb..c7071484ca 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components public class LightSquare : Star { - private Box box; + private readonly Box box; public LightSquare() { diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 78d27c87ff..ad00dffac7 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -15,7 +15,6 @@ using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Gameplay.Components; using osu.Game.Tournament.Screens.MapPool; using osu.Game.Tournament.Screens.TeamWin; -using osuTK; using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay @@ -72,10 +71,9 @@ namespace osu.Game.Tournament.Screens.Gameplay }, scoreDisplay = new MatchScoreDisplay { - Y = -60, - Scale = new Vector2(0.8f), + Y = -147, Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Origin = Anchor.TopCentre, }, new ControlPanel { From 979988235dab7aa08e138e9a1e5f20eeadd6ad67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 12:49:09 +0900 Subject: [PATCH 140/387] Increase flexibility of StarCounter component --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 12 +-- .../Graphics/UserInterface/StarCounter.cs | 101 ++++++++++-------- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index ffd6f55b53..88bb83b446 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -160), - CountStars = 5, + Current = 5, }; Add(stars); @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -190), - Text = stars.CountStars.ToString("0.00"), + Text = stars.Current.ToString("0.00"), }; Add(starsLabel); @@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Gameplay comboCounter.Current.Value = 0; numerator = denominator = 0; accuracyCounter.SetFraction(0, 0); - stars.CountStars = 0; - starsLabel.Text = stars.CountStars.ToString("0.00"); + stars.Current = 0; + starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Hit! :D", delegate @@ -91,8 +91,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep(@"Alter stars", delegate { - stars.CountStars = RNG.NextSingle() * (stars.StarCount + 1); - starsLabel.Text = stars.CountStars.ToString("0.00"); + stars.Current = RNG.NextSingle() * (stars.StarCount + 1); + starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Stop counters", delegate diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index 586cd2ce84..b13d6485ac 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -13,7 +13,7 @@ namespace osu.Game.Graphics.UserInterface { public class StarCounter : Container { - private readonly Container stars; + private readonly FillFlowContainer stars; /// /// Maximum amount of stars displayed. @@ -23,34 +23,29 @@ namespace osu.Game.Graphics.UserInterface /// public int StarCount { get; } - private double animationDelay => 80; + /// + /// The added delay for each subsequent star to be animated. + /// + protected virtual double AnimationDelay => 80; - private double scalingDuration => 1000; - private Easing scalingEasing => Easing.OutElasticHalf; - private float minStarScale => 0.4f; - - private double fadingDuration => 100; - private float minStarAlpha => 0.5f; - - private const float star_size = 20; private const float star_spacing = 4; - private float countStars; + private float current; /// /// Amount of stars represented. /// - public float CountStars + public float Current { - get => countStars; + get => current; set { - if (countStars == value) return; + if (current == value) return; if (IsLoaded) - transformCount(value); - countStars = value; + animate(value); + current = value; } } @@ -71,11 +66,13 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(star_spacing), - ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => new Star { Alpha = minStarAlpha }) + ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => CreateStar()) } }; } + public virtual Star CreateStar() => new DefaultStar(); + protected override void LoadComplete() { base.LoadComplete(); @@ -86,63 +83,60 @@ namespace osu.Game.Graphics.UserInterface public void ResetCount() { - countStars = 0; + current = 0; StopAnimation(); } public void ReplayAnimation() { - var t = countStars; + var t = current; ResetCount(); - CountStars = t; + Current = t; } public void StopAnimation() { - int i = 0; - + animate(current); foreach (var star in stars.Children) + star.FinishTransforms(true); + } + + private float getStarScale(int i, float value) => i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, 0, 1.0f, i, i + 1); + + private void animate(float newValue) + { + for (var i = 0; i < stars.Children.Count; i++) { + var star = stars.Children[i]; + star.ClearTransforms(true); - star.FadeTo(i < countStars ? 1.0f : minStarAlpha); - star.Icon.ScaleTo(getStarScale(i, countStars)); - i++; + + double delay = (current <= newValue ? Math.Max(i - current, 0) : Math.Max(current - 1 - i, 0)) * AnimationDelay; + + using (star.BeginDelayedSequence(delay, true)) + star.DisplayAt(getStarScale(i, newValue)); } } - private float getStarScale(int i, float value) + public class DefaultStar : Star { - if (value <= i) - return minStarScale; + private const double scaling_duration = 1000; - return i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, minStarScale, 1.0f, i, i + 1); - } + private const double fading_duration = 100; - private void transformCount(float newValue) - { - int i = 0; + private const Easing scaling_easing = Easing.OutElasticHalf; - foreach (var star in stars.Children) - { - star.ClearTransforms(true); + private const float min_star_scale = 0.4f; - var delay = (countStars <= newValue ? Math.Max(i - countStars, 0) : Math.Max(countStars - 1 - i, 0)) * animationDelay; - star.Delay(delay).FadeTo(i < newValue ? 1.0f : minStarAlpha, fadingDuration); - star.Icon.Delay(delay).ScaleTo(getStarScale(i, newValue), scalingDuration, scalingEasing); + private const float star_size = 20; - i++; - } - } - - private class Star : Container - { public readonly SpriteIcon Icon; - public Star() + public DefaultStar() { Size = new Vector2(star_size); - Child = Icon = new SpriteIcon + InternalChild = Icon = new SpriteIcon { Size = new Vector2(star_size), Icon = FontAwesome.Solid.Star, @@ -150,6 +144,19 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.Centre, }; } + + public override void DisplayAt(float scale) + { + scale = Math.Clamp(scale, min_star_scale, 1); + + this.FadeTo(scale, fading_duration); + Icon.ScaleTo(scale, scaling_duration, scaling_easing); + } + } + + public abstract class Star : CompositeDrawable + { + public abstract void DisplayAt(float scale); } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d9eeec9f85..50419a5fb9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Select.Carousel }, starCounter = new StarCounter { - CountStars = (float)beatmap.StarDifficulty, + Current = (float)beatmap.StarDifficulty, Scale = new Vector2(0.8f), } } From 059aea8ead39601ffe0ff79d21333bb0443045b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:09:07 +0900 Subject: [PATCH 141/387] Initial pass of schedule screen design update --- .../Screens/Schedule/ScheduleScreen.cs | 126 +++++++++++++----- 1 file changed, 95 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 4c93c04fcf..9ca1aa626a 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -18,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Schedule { - public class ScheduleScreen : TournamentScreen + public class ScheduleScreen : TournamentScreen // IProvidesVideo { private readonly Bindable currentMatch = new Bindable(); private Container mainContainer; @@ -38,10 +39,63 @@ namespace osu.Game.Tournament.Screens.Schedule RelativeSizeAxes = Axes.Both, Loop = true, }, - mainContainer = new Container + new Container { RelativeSizeAxes = Axes.Both, - } + Padding = new MarginPadding(100) { Bottom = 50 }, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DrawableTournamentTitleText(), + new Container + { + Margin = new MarginPadding { Top = 40 }, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.White, + Size = new Vector2(50, 10), + }, + new TournamentSpriteTextWithBackground("Schedule") + { + X = 60, + Scale = new Vector2(0.8f) + } + } + }, + } + }, + }, + new Drawable[] + { + mainContainer = new Container + { + RelativeSizeAxes = Axes.Both, + } + } + } + } + } + }, }; currentMatch.BindValueChanged(matchChanged); @@ -91,7 +145,7 @@ namespace osu.Game.Tournament.Screens.Schedule .Take(8) .Select(p => new ScheduleMatch(p)) }, - new ScheduleContainer("match overview") + new ScheduleContainer("upcoming matches") { RelativeSizeAxes = Axes.Both, Width = 0.6f, @@ -100,26 +154,45 @@ namespace osu.Game.Tournament.Screens.Schedule } } }, - new ScheduleContainer("current match") + new ScheduleContainer("coming up next") { RelativeSizeAxes = Axes.Both, Height = 0.25f, Children = new Drawable[] { - new TournamentSpriteText + new FillFlowContainer { - Margin = new MarginPadding { Left = -10, Bottom = 10, Top = -5 }, - Spacing = new Vector2(10, 0), - Text = match.NewValue.Round.Value?.Name.Value, - Colour = Color4.Black, - Font = OsuFont.Torus.With(size: 20) - }, - new ScheduleMatch(match.NewValue, false), - new TournamentSpriteText - { - Text = "Start Time " + match.NewValue.Date.Value.ToUniversalTime().ToString("HH:mm UTC"), - Colour = Color4.Black, - Font = OsuFont.Torus.With(size: 20) + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(30), + Children = new Drawable[] + { + new ScheduleMatch(match.NewValue, false) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.5f) + }, + new TournamentSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName, + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold) + }, + new TournamentSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = $"Starting {match.NewValue.Date.Value.Humanize()}", + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) + }, + } }, } } @@ -170,29 +243,20 @@ namespace osu.Game.Tournament.Screens.Schedule public ScheduleContainer(string title) { - Padding = new MarginPadding { Left = 30, Top = 30 }; + Padding = new MarginPadding { Left = 30, Top = 10 }; InternalChildren = new Drawable[] { - new TournamentSpriteText + new TournamentSpriteTextWithBackground(title.ToUpperInvariant()) { X = 30, - Text = title, - Colour = Color4.Black, - Spacing = new Vector2(10, 0), - Font = OsuFont.Torus.With(size: 30) + Scale = new Vector2(0.5f) }, content = new FillFlowContainer { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, - Margin = new MarginPadding(40) + Margin = new MarginPadding(10) }, - new Circle - { - Colour = new Color4(233, 187, 79, 255), - Width = 5, - RelativeSizeAxes = Axes.Y, - } }; } } From 434feb5ac6b1986d27eb2907ca042be60175759b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 13:26:54 +0900 Subject: [PATCH 142/387] Fix alignment on schedule screen --- .../Screens/TestSceneScheduleScreen.cs | 3 ++ .../Components/DrawableTournamentMatch.cs | 12 +++---- .../Screens/Schedule/ScheduleScreen.cs | 36 ++++++++++++------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs index 2277302e98..b240ef3ae5 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Schedule; namespace osu.Game.Tournament.Tests.Screens @@ -11,6 +13,7 @@ namespace osu.Game.Tournament.Tests.Screens [BackgroundDependencyLoader] private void load() { + Add(new TourneyVideo("main") { RelativeSizeAxes = Axes.Both }); Add(new ScheduleScreen()); } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index c4b670f059..d0ba9a96f4 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private readonly bool editor; protected readonly FillFlowContainer Flow; private readonly Drawable selectionBox; - private readonly Drawable currentMatchSelectionBox; + protected readonly Drawable CurrentMatchSelectionBox; private Bindable globalSelection; [Resolved(CanBeNull = true)] @@ -55,11 +55,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components Colour = Color4.YellowGreen, Child = new Box { RelativeSizeAxes = Axes.Both } }, - currentMatchSelectionBox = new Container + CurrentMatchSelectionBox = new Container { - CornerRadius = 5, - Masking = true, - Scale = new Vector2(1.05f), + Scale = new Vector2(1.05f, 1.1f), RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -128,9 +126,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private void updateCurrentMatch() { if (Match.Current.Value) - currentMatchSelectionBox.Show(); + CurrentMatchSelectionBox.Show(); else - currentMatchSelectionBox.Hide(); + CurrentMatchSelectionBox.Hide(); } private bool selected; diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 9ca1aa626a..634ceb11a5 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tournament.Screens.Schedule new Container { RelativeSizeAxes = Axes.Both, - Height = 0.65f, + Height = 0.74f, Child = new FillFlowContainer { RelativeSizeAxes = Axes.Both, @@ -162,7 +162,7 @@ namespace osu.Game.Tournament.Screens.Schedule { new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(30), Children = new Drawable[] @@ -207,6 +207,8 @@ namespace osu.Game.Tournament.Screens.Schedule { Flow.Direction = FillDirection.Horizontal; + Scale = new Vector2(0.8f); + bool conditional = match is ConditionalTournamentMatch; if (conditional) @@ -218,15 +220,16 @@ namespace osu.Game.Tournament.Screens.Schedule { Anchor = Anchor.TopRight, Origin = Anchor.TopLeft, - Colour = Color4.Black, + Colour = OsuColour.Gray(0.7f), Alpha = conditional ? 0.6f : 1, + Font = OsuFont.Torus, Margin = new MarginPadding { Horizontal = 10, Vertical = 5 }, }); AddInternal(new TournamentSpriteText { Anchor = Anchor.BottomRight, Origin = Anchor.BottomLeft, - Colour = Color4.Black, + Colour = OsuColour.Gray(0.7f), Alpha = conditional ? 0.6f : 1, Margin = new MarginPadding { Horizontal = 10, Vertical = 5 }, Text = match.Date.Value.ToUniversalTime().ToString("HH:mm UTC") + (conditional ? " (conditional)" : "") @@ -243,19 +246,26 @@ namespace osu.Game.Tournament.Screens.Schedule public ScheduleContainer(string title) { - Padding = new MarginPadding { Left = 30, Top = 10 }; + Padding = new MarginPadding { Left = 60, Top = 10 }; InternalChildren = new Drawable[] { - new TournamentSpriteTextWithBackground(title.ToUpperInvariant()) + new FillFlowContainer { - X = 30, - Scale = new Vector2(0.5f) - }, - content = new FillFlowContainer - { - Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, - Margin = new MarginPadding(10) + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TournamentSpriteTextWithBackground(title.ToUpperInvariant()) + { + Scale = new Vector2(0.5f) + }, + content = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.Both, + Margin = new MarginPadding(10) + }, + } }, }; } From 9934a97bd0a9dc3a7eb97113b7a74af88fdb004a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 16:31:28 +0900 Subject: [PATCH 143/387] Limit upcoming matches displayed to 8 --- osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 634ceb11a5..01d6d33fee 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tournament.Screens.Schedule .SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a)))); upcoming = upcoming.Concat(conditionals); - upcoming = upcoming.OrderBy(p => p.Date.Value).Take(12); + upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8); mainContainer.Child = new FillFlowContainer { From 6c0a27e0b956e0ff955c77be57981614a680be6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 16:38:50 +0900 Subject: [PATCH 144/387] Improve look of selected match --- .../Screens/Ladder/Components/DrawableTournamentMatch.cs | 2 +- osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index d0ba9a96f4..320e76775a 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, - Colour = Color4.OrangeRed, + Colour = Color4.White, Child = new Box { RelativeSizeAxes = Axes.Both } }, Flow = new FillFlowContainer diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 01d6d33fee..27a1fe98d2 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -209,6 +209,8 @@ namespace osu.Game.Tournament.Screens.Schedule Scale = new Vector2(0.8f); + CurrentMatchSelectionBox.Scale = new Vector2(1.02f, 1.15f); + bool conditional = match is ConditionalTournamentMatch; if (conditional) From 8ab9ca77d60b4d8c065454b5f495ec87e75bc0cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 15:06:06 +0900 Subject: [PATCH 145/387] Fix next match timer not updating --- .../Screens/Schedule/ScheduleScreen.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 27a1fe98d2..0fcec645e3 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -185,12 +184,24 @@ namespace osu.Game.Tournament.Screens.Schedule Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName, Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold) }, - new TournamentSpriteText + new FillFlowContainer { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = $"Starting {match.NewValue.Date.Value.Humanize()}", - Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) + Children = new Drawable[] + { + new TournamentSpriteText + { + Text = "Starting ", + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) + }, + new DrawableDate(match.NewValue.Date.Value) + { + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) + } + } }, } }, From 3ac599246dc816247c7790aec8a95e0fc50aa62b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:08:24 +0900 Subject: [PATCH 146/387] Initial pass of seeding screen design update --- .../Screens/TeamIntro/SeedingScreen.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index 513d84b594..d48e396b89 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -15,7 +15,6 @@ using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Ladder.Components; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.TeamIntro { @@ -140,9 +139,9 @@ namespace osu.Game.Tournament.Screens.TeamIntro Spacing = new Vector2(5), Children = new Drawable[] { - new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Title, Colour = Color4.Black, }, - new TournamentSpriteText { Text = "by", Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, - new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Artist, Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Title, Colour = TournamentGame.TEXT_COLOUR, }, + new TournamentSpriteText { Text = "by", Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Artist, Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, } }, new FillFlowContainer @@ -154,8 +153,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro Spacing = new Vector2(40), Children = new Drawable[] { - new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = Color4.Black, Width = 80 }, - new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 }, + new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, } }, }; @@ -204,7 +203,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, + Colour = TournamentGame.TEXT_COLOUR, }, new TournamentSpriteText { @@ -260,20 +259,18 @@ namespace osu.Game.Tournament.Screens.TeamIntro AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; - var colour = OsuColour.Gray(0.3f); - InternalChildren = new Drawable[] { new TournamentSpriteText { Text = left, - Colour = colour, - Font = OsuFont.Torus.With(size: 22), + Colour = TournamentGame.TEXT_COLOUR, + Font = OsuFont.Torus.With(size: 22, weight: FontWeight.SemiBold), }, new TournamentSpriteText { Text = right, - Colour = colour, + Colour = TournamentGame.TEXT_COLOUR, Anchor = Anchor.TopRight, Origin = Anchor.TopLeft, Font = OsuFont.Torus.With(size: 22, weight: FontWeight.Regular), @@ -305,7 +302,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro { Text = team?.FullName.Value ?? "???", Font = OsuFont.Torus.With(size: 32, weight: FontWeight.SemiBold), - Colour = Color4.Black, + Colour = TournamentGame.TEXT_COLOUR, }, } }; From 3a3a2ad2a71126973caac66e7b98ee7acf938caa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 14:47:29 +0900 Subject: [PATCH 147/387] Fix video looping not propagating when set too early in initialisation --- osu.Game.Tournament/Components/TourneyVideo.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 7d2eaff515..786b7b3c67 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -38,7 +38,8 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, - Clock = new FramedClock(manualClock = new ManualClock()) + Clock = new FramedClock(manualClock = new ManualClock()), + Loop = loop, }; } else if (drawFallbackGradient) @@ -51,10 +52,13 @@ namespace osu.Game.Tournament.Components } } + private bool loop; + public bool Loop { set { + loop = value; if (video != null) video.Loop = value; } From a85cef2f064640f6a5d7029b65aded95e9ed353b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 14:23:13 +0900 Subject: [PATCH 148/387] Reset win screen video on display; add fade in transition --- osu.Game.Tournament/Components/TourneyVideo.cs | 2 ++ .../Screens/TeamWin/TeamWinScreen.cs | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 7d2eaff515..9b1350ca23 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -60,6 +60,8 @@ namespace osu.Game.Tournament.Components } } + public void Reset() => manualClock.CurrentTime = 0; + protected override void Update() { base.Update(); diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 8b3f4488d0..3870f486e1 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -62,7 +62,9 @@ namespace osu.Game.Tournament.Screens.TeamWin update(); } - private void update() + private bool firstDisplay = true; + + private void update() => Schedule(() => { var match = currentMatch.Value; @@ -75,6 +77,15 @@ namespace osu.Game.Tournament.Screens.TeamWin redWinVideo.Alpha = match.WinnerColour == TeamColour.Red ? 1 : 0; blueWinVideo.Alpha = match.WinnerColour == TeamColour.Blue ? 1 : 0; + if (firstDisplay) + { + if (match.WinnerColour == TeamColour.Red) + redWinVideo.Reset(); + else + blueWinVideo.Reset(); + firstDisplay = false; + } + mainContainer.Children = new Drawable[] { new DrawableTeamFlag(match.Winner) @@ -108,6 +119,8 @@ namespace osu.Game.Tournament.Screens.TeamWin } }, }; - } + mainContainer.FadeOut(); + mainContainer.Delay(2000).FadeIn(1600, Easing.OutQuint); + }); } } From 5d5910822bc6d45d595f2eb83332b17bdfeb3743 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:08:12 +0900 Subject: [PATCH 149/387] Initial pass of intro screen design update --- .../Screens/TeamIntro/TeamIntroScreen.cs | 153 +++--------------- 1 file changed, 22 insertions(+), 131 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index d584c21058..2daf9d35f3 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; -using osu.Game.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; @@ -49,141 +48,33 @@ namespace osu.Game.Tournament.Screens.TeamIntro return; } + const float y_flag_offset = 288; + + const float y_offset = 460; + mainContainer.Children = new Drawable[] { - new TeamWithPlayers(match.NewValue.Team1.Value, true) - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Height = 0.6f, - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight - }, - new TeamWithPlayers(match.NewValue.Team2.Value) - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Height = 0.6f, - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft - }, new RoundDisplay(match.NewValue) { - RelativeSizeAxes = Axes.Both, - Height = 0.25f, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 180, - } + Position = new Vector2(100, 100) + }, + new DrawableTeamFlag(match.NewValue.Team1.Value) + { + Position = new Vector2(160, y_flag_offset), + }, + new DrawableTeamWithPlayers(match.NewValue.Team1.Value, TeamColour.Red) + { + Position = new Vector2(160, y_offset), + }, + new DrawableTeamFlag(match.NewValue.Team2.Value) + { + Position = new Vector2(740, y_flag_offset), + }, + new DrawableTeamWithPlayers(match.NewValue.Team2.Value, TeamColour.Blue) + { + Position = new Vector2(740, y_offset), + }, }; } - - private class RoundDisplay : CompositeDrawable - { - public RoundDisplay(TournamentMatch match) - { - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new TournamentSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = OsuColour.Gray(0.33f), - Text = match.Round.Value?.Name.Value ?? "Unknown Round", - Font = OsuFont.Torus.With(size: 26, weight: FontWeight.Light) - }, - } - } - }; - } - } - - private class TeamWithPlayers : CompositeDrawable - { - public TeamWithPlayers(TournamentTeam team, bool left = false) - { - FillFlowContainer players; - var colour = left ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; - InternalChildren = new Drawable[] - { - new TeamDisplay(team) - { - Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = Anchor.TopCentre, - RelativePositionAxes = Axes.Both, - X = (left ? -1 : 1) * 0.3145f, - Y = -0.077f, - }, - players = new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 5), - Padding = new MarginPadding(20), - Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, - RelativePositionAxes = Axes.Both, - X = (left ? -1 : 1) * 0.58f, - }, - }; - - if (team != null) - { - foreach (var p in team.Players) - { - players.Add(new TournamentSpriteText - { - Text = p.Username, - Font = OsuFont.Torus.With(size: 24), - Colour = colour, - Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, - }); - } - } - } - - private class TeamDisplay : DrawableTournamentTeam - { - public TeamDisplay(TournamentTeam team) - : base(team) - { - AutoSizeAxes = Axes.Both; - - Flag.Anchor = Flag.Origin = Anchor.TopCentre; - Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(300, 200); - Flag.Scale = new Vector2(0.32f); - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(160), - Children = new Drawable[] - { - Flag, - new TournamentSpriteText - { - Text = team?.FullName.Value ?? "???", - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Regular), - Colour = OsuColour.Gray(0.2f), - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - }, - } - }; - } - } - } } } From 8e4b15aaa5f0a4e3aeaf66a85d628ecf9f7df54d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 17:43:55 +0900 Subject: [PATCH 150/387] Update test scene --- osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index ae7d9d853a..34fa7a4997 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Tournament.Components; +using osu.Game.Tournament.Screens; using osu.Game.Tournament.Screens.Gameplay; using osu.Game.Tournament.Screens.Gameplay.Components; @@ -13,7 +14,7 @@ namespace osu.Game.Tournament.Tests.Screens public class TestSceneGameplayScreen : TournamentTestScene { [Cached] - private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); + private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay { Width = 0.5f }; public override IReadOnlyList RequiredTypes => new[] { @@ -22,6 +23,8 @@ namespace osu.Game.Tournament.Tests.Screens typeof(TeamDisplay), typeof(MatchHeader), typeof(MatchScoreDisplay), + typeof(BeatmapInfoScreen), + typeof(SongBar), }; [BackgroundDependencyLoader] From 0102aaf32a53e6c2a0714ca79a85ca7b24fe949d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 18:11:57 +0900 Subject: [PATCH 151/387] Move chat expand/contract logic local to tournament --- .../Components/TournamentMatchChatDisplay.cs | 4 ++++ osu.Game/Online/Chat/StandAloneChatDisplay.cs | 12 ------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 48c5b9bd35..f9cd18be2c 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -66,6 +66,10 @@ namespace osu.Game.Tournament.Components } } + public void Expand() => this.FadeIn(300); + + public void Contract() => this.FadeOut(200); + protected override ChatLine CreateMessage(Message message) => new MatchMessage(message); protected class MatchMessage : StandAloneMessage diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 881dd19d8e..0914f688e9 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -92,18 +92,6 @@ namespace osu.Game.Online.Chat textbox.Text = string.Empty; } - public void Expand() - { - this.FadeIn(300); - this.MoveToY(0, 500, Easing.OutQuint); - } - - public void Contract() - { - this.FadeOut(200); - this.MoveToY(100, 500, Easing.In); - } - protected virtual ChatLine CreateMessage(Message message) => new StandAloneMessage(message); private void channelChanged(ValueChangedEvent e) From 3744aaf55f228462c7b1227f1034fb137197eaa1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 18:16:32 +0900 Subject: [PATCH 152/387] Update vertical alignment of chroma area --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index ad00dffac7..4d770855cd 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -52,9 +52,9 @@ namespace osu.Game.Tournament.Screens.Gameplay { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Y = 5, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Y = 110, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Children = new Drawable[] { new Box From 8b0b910196f693343a7feb3a21be0c49e0a6de85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 14:46:09 +0900 Subject: [PATCH 153/387] Update song / chat / beatmap info display to reflect new design --- .../Screens/TestSceneGameplayScreen.cs | 1 + osu.Game.Tournament/Components/SongBar.cs | 204 ++++++++---------- .../Components/TournamentBeatmapPanel.cs | 8 +- .../Components/TournamentMatchChatDisplay.cs | 3 +- .../Screens/BeatmapInfoScreen.cs | 1 + osu.Game.Tournament/TournamentSceneManager.cs | 7 +- 6 files changed, 107 insertions(+), 117 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 34fa7a4997..1e20687a87 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -35,3 +35,4 @@ namespace osu.Game.Tournament.Tests.Screens } } } + diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 48ea36a8f3..8d766ec9ba 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -4,10 +4,8 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; @@ -24,6 +22,8 @@ namespace osu.Game.Tournament.Components { private BeatmapInfo beatmap; + private const float height = 145; + [Resolved] private IBindable ruleset { get; set; } @@ -52,15 +52,7 @@ namespace osu.Game.Tournament.Components } } - private Container panelContents; - private Container innerPanel; - private Container outerPanel; - private TournamentBeatmapPanel panel; - - private float panelWidth => expanded ? 0.6f : 1; - - private const float main_width = 0.97f; - private const float inner_panel_width = 0.7f; + private FillFlowContainer flow; private bool expanded; @@ -70,86 +62,27 @@ namespace osu.Game.Tournament.Components set { expanded = value; - panel?.ResizeWidthTo(panelWidth, 800, Easing.OutQuint); - - if (expanded) - { - innerPanel.ResizeWidthTo(inner_panel_width, 800, Easing.OutQuint); - outerPanel.ResizeWidthTo(main_width, 800, Easing.OutQuint); - } - else - { - innerPanel.ResizeWidthTo(1, 800, Easing.OutQuint); - outerPanel.ResizeWidthTo(0.25f, 800, Easing.OutQuint); - } + flow.Direction = expanded ? FillDirection.Full : FillDirection.Vertical; } } [BackgroundDependencyLoader] private void load() { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] { - outerPanel = new Container + flow = new FillFlowContainer { - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.2f), - Type = EdgeEffectType.Shadow, - Radius = 5, - }, RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + LayoutDuration = 500, + LayoutEasing = Easing.OutQuint, + Direction = FillDirection.Full, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - RelativePositionAxes = Axes.X, - X = -(1 - main_width) / 2, - Y = -10, - Width = main_width, - Height = TournamentBeatmapPanel.HEIGHT, - CornerRadius = TournamentBeatmapPanel.HEIGHT / 2, - CornerExponent = 2, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.93f), - }, - new OsuLogo - { - Triangles = false, - Colour = OsuColour.Gray(0.33f), - Scale = new Vector2(0.08f), - Margin = new MarginPadding(50), - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }, - innerPanel = new Container - { - Masking = true, - CornerRadius = TournamentBeatmapPanel.HEIGHT / 2, - CornerExponent = 2, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = inner_panel_width, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.86f), - }, - panelContents = new Container - { - RelativeSizeAxes = Axes.Both, - } - } - } - } } }; @@ -160,7 +93,7 @@ namespace osu.Game.Tournament.Components { if (beatmap == null) { - panelContents.Clear(); + flow.Clear(); return; } @@ -219,34 +152,86 @@ namespace osu.Game.Tournament.Components break; } - panelContents.Children = new Drawable[] + flow.Children = new Drawable[] { - new DiffPiece(("Length", TimeSpan.FromMilliseconds(length).ToString(@"mm\:ss"))) + new Container { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = height / 2, + Width = 0.5f, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DiffPiece(stats), + new DiffPiece(("Star Rating", $"{beatmap.StarDifficulty:0.#}{srExtra}")) + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DiffPiece(("Length", TimeSpan.FromMilliseconds(length).ToString(@"mm\:ss"))), + new DiffPiece(("BPM", $"{bpm:0.#}")) + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, + }, + new OsuLogo + { + Triangles = false, + Scale = new Vector2(0.08f), + Margin = new MarginPadding(50), + X = -10, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + } + }, + }, + } + } + } }, - new DiffPiece(("BPM", $"{bpm:0.#}")) + new TournamentBeatmapPanel(beatmap) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopLeft - }, - new DiffPiece(stats) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.BottomRight - }, - new DiffPiece(("Star Rating", $"{beatmap.StarDifficulty:0.#}{srExtra}")) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.TopRight - }, - panel = new TournamentBeatmapPanel(beatmap) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(panelWidth, 1) + RelativeSizeAxes = Axes.X, + Width = 0.5f, + Height = height / 2, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, } }; } @@ -258,10 +243,9 @@ namespace osu.Game.Tournament.Components Margin = new MarginPadding { Horizontal = 15, Vertical = 1 }; AutoSizeAxes = Axes.Both; - static void cp(SpriteText s, Color4 colour) + static void cp(SpriteText s, bool bold) { - s.Colour = colour; - s.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 15); + s.Font = OsuFont.Torus.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, size: 15); } for (var i = 0; i < tuples.Length; i++) @@ -272,14 +256,14 @@ namespace osu.Game.Tournament.Components { AddText(" / ", s => { - cp(s, OsuColour.Gray(0.33f)); + cp(s, false); s.Spacing = new Vector2(-2, 0); }); } - AddText(new TournamentSpriteText { Text = heading }, s => cp(s, OsuColour.Gray(0.33f))); - AddText(" ", s => cp(s, OsuColour.Gray(0.33f))); - AddText(new TournamentSpriteText { Text = content }, s => cp(s, OsuColour.Gray(0.5f))); + AddText(new TournamentSpriteText { Text = heading }, s => cp(s, false)); + AddText(" ", s => cp(s, false)); + AddText(new TournamentSpriteText { Text = content }, s => cp(s, true)); } } } diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 394ffe304e..e09af06c89 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -70,8 +70,8 @@ namespace osu.Game.Tournament.Components new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Padding = new MarginPadding(vertical_padding), Direction = FillDirection.Vertical, Children = new Drawable[] @@ -137,8 +137,8 @@ namespace osu.Game.Tournament.Components Texture = textures.Get($"mods/{mods}"), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Margin = new MarginPadding(20), - Scale = new Vector2(0.5f) + Margin = new MarginPadding(10), + Scale = new Vector2(0.8f) }); } } diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index f4fd27784c..8eb1c98ba0 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -9,7 +9,6 @@ using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; -using osuTK; namespace osu.Game.Tournament.Components { @@ -22,7 +21,7 @@ namespace osu.Game.Tournament.Components public TournamentMatchChatDisplay() { RelativeSizeAxes = Axes.X; - Size = new Vector2(0.5f, 142); + Height = 144; Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index fccd35ca9e..0a3163ef43 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tournament.Screens { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, + Depth = float.MinValue, }); } diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 287e25b1fb..ef8d16011d 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -194,9 +194,14 @@ namespace osu.Game.Tournament switch (currentScreen) { - case GameplayScreen _: case MapPoolScreen _: chatContainer.FadeIn(TournamentScreen.FADE_DELAY); + chatContainer.ResizeWidthTo(1, 500, Easing.OutQuint); + break; + + case GameplayScreen _: + chatContainer.FadeIn(TournamentScreen.FADE_DELAY); + chatContainer.ResizeWidthTo(0.5f, 500, Easing.OutQuint); break; default: From 9138bafbeb5012f6af842731fd4383e5edecadaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 18:40:13 +0900 Subject: [PATCH 154/387] Fix alignment of flags on team intro screen --- osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index 2daf9d35f3..6c2848897b 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro return; } - const float y_flag_offset = 288; + const float y_flag_offset = 292; const float y_offset = 460; @@ -60,11 +60,11 @@ namespace osu.Game.Tournament.Screens.TeamIntro }, new DrawableTeamFlag(match.NewValue.Team1.Value) { - Position = new Vector2(160, y_flag_offset), + Position = new Vector2(165, y_flag_offset), }, new DrawableTeamWithPlayers(match.NewValue.Team1.Value, TeamColour.Red) { - Position = new Vector2(160, y_offset), + Position = new Vector2(165, y_offset), }, new DrawableTeamFlag(match.NewValue.Team2.Value) { From 2fe32b7d2b3b584335101c8036a32cbeb108d542 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 19:41:22 +0900 Subject: [PATCH 155/387] Remove LadderInfo requirement in DrawableMatchTeam --- .../Screens/Ladder/Components/DrawableMatchTeam.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 88d7b95b0c..38e906e07b 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -37,11 +37,13 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private readonly Func isWinner; private LadderEditorScreen ladderEditor; - [Resolved] + [Resolved(canBeNull: true)] private LadderInfo ladderInfo { get; set; } private void setCurrent() { + if (ladderInfo == null) return; + //todo: tournamentgamebase? if (ladderInfo.CurrentMatch.Value != null) ladderInfo.CurrentMatch.Value.Current.Value = false; From cc5cae4db995d6b680022e11b1ba1f272983d3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Mar 2020 14:08:49 +0100 Subject: [PATCH 156/387] Do not transition to result screen --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 4bc00425bf..8b8070caf1 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -36,6 +36,11 @@ namespace osu.Game.Tests.Visual private class PerfectModTestPlayer : TestPlayer { + public PerfectModTestPlayer() + : base(showResults: false) + { + } + protected override bool AllowFail => true; public bool CheckFailed(bool failed) From c803de2b499a44a8dde5eba44961ddcea684fb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Mar 2020 14:18:45 +0100 Subject: [PATCH 157/387] Fix player instantiation Since ModTestScene.CreatePlayer would apply mods in addition to instantiating the player, overriding it could lead to mistakenly also overriding the code that was supposed to set up the test via currentTestData. Make ModTestScene.CreatePlayer sealed, which ensures that mod & autoplay changes are applied, and expose ModTestScene.CreateModPlayer instead which has the expected semantics. --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- osu.Game/Tests/Visual/ModTestScene.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 8b8070caf1..d6255d2478 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testCaseData.FailOnMiss) }); - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PerfectModTestPlayer(); + protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(); private class PerfectModTestPlayer : TestPlayer { diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index eb418304d9..8b41fb5075 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); - protected override TestPlayer CreatePlayer(Ruleset ruleset) + protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) { var mods = new List(SelectedMods.Value); @@ -57,9 +57,11 @@ namespace osu.Game.Tests.Visual SelectedMods.Value = mods; - return new ModTestPlayer(AllowFail); + return CreateModPlayer(ruleset); } + protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(AllowFail); + protected class ModTestPlayer : TestPlayer { protected override bool AllowFail { get; } From 0953751d242d4b6a850c4dbd9066d0de1aad125a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Mar 2020 15:51:57 +0100 Subject: [PATCH 158/387] Clamp relative position of judgement ticks in range [0;1] --- osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 208bdd17ad..c7f763e9ab 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.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 System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -224,7 +225,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters , arrow_move_duration, Easing.Out); } - private float getRelativeJudgementPosition(double value) => (float)((value / maxHitWindow) + 1) / 2; + private float getRelativeJudgementPosition(double value) => Math.Clamp((float)((value / maxHitWindow) + 1) / 2, 0, 1); private class JudgementLine : CompositeDrawable { From 414e704d37c456ee10ac333f9e574c17607cd502 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 00:18:28 +0900 Subject: [PATCH 159/387] Use existing local function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs index 0b80bef903..e949bf9881 100644 --- a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs @@ -46,12 +46,7 @@ namespace osu.Game.Tournament.Components { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - ChildrenEnumerable = team?.Players.Select(p => new TournamentSpriteText - { - Text = p.Username, - Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), - Colour = Color4.White, - }).Skip(5) ?? Enumerable.Empty() + ChildrenEnumerable = team?.Players.Select(createPlayerText).Skip(5) ?? Enumerable.Empty() }, } }, From 61297847a74e7412da247a5223e1e01b57eb68c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 01:21:37 +0900 Subject: [PATCH 160/387] Fix compilation failure --- osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index ce17c392d0..01be91dfe5 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentTeamScore.BindTo(score); } - private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; + private void scoreChanged(ValueChangedEvent score) => counter.Current = score.NewValue ?? 0; } private class TeamDisplay : DrawableTournamentTeam From c2fbc85e7706a90c715096fe9cc722662d97b287 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 01:26:19 +0900 Subject: [PATCH 161/387] Split out test scene for StarCounter --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 37 ------------ .../Visual/Gameplay/TestSceneStarCounter.cs | 57 +++++++++++++++++++ 2 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index 88bb83b446..030d420ec0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -3,9 +3,6 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Utils; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; using osuTK; @@ -45,32 +42,12 @@ namespace osu.Game.Tests.Visual.Gameplay }; Add(accuracyCounter); - StarCounter stars = new StarCounter - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Position = new Vector2(20, -160), - Current = 5, - }; - Add(stars); - - SpriteText starsLabel = new OsuSpriteText - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Position = new Vector2(20, -190), - Text = stars.Current.ToString("0.00"), - }; - Add(starsLabel); - AddStep(@"Reset all", delegate { score.Current.Value = 0; comboCounter.Current.Value = 0; numerator = denominator = 0; accuracyCounter.SetFraction(0, 0); - stars.Current = 0; - starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Hit! :D", delegate @@ -88,20 +65,6 @@ namespace osu.Game.Tests.Visual.Gameplay denominator++; accuracyCounter.SetFraction(numerator, denominator); }); - - AddStep(@"Alter stars", delegate - { - stars.Current = RNG.NextSingle() * (stars.StarCount + 1); - starsLabel.Text = stars.Current.ToString("0.00"); - }); - - AddStep(@"Stop counters", delegate - { - score.StopRolling(); - comboCounter.StopRolling(); - accuracyCounter.StopRolling(); - stars.StopAnimation(); - }); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs new file mode 100644 index 0000000000..709e71d195 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [TestFixture] + public class TestSceneStarCounter : OsuTestScene + { + public TestSceneStarCounter() + { + StarCounter stars = new StarCounter + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Current = 5, + }; + + Add(stars); + + SpriteText starsLabel = new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Scale = new Vector2(2), + Y = 50, + Text = stars.Current.ToString("0.00"), + }; + + Add(starsLabel); + + AddRepeatStep(@"random value", delegate + { + stars.Current = RNG.NextSingle() * (stars.StarCount + 1); + starsLabel.Text = stars.Current.ToString("0.00"); + }, 10); + + AddStep(@"Stop animation", delegate + { + stars.StopAnimation(); + }); + + AddStep(@"Reset", delegate + { + stars.Current = 0; + starsLabel.Text = stars.Current.ToString("0.00"); + }); + } + } +} From 3903423a37d53b60cd690c1583c968842f2fcdc4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 8 Mar 2020 19:43:53 -0700 Subject: [PATCH 162/387] Fix textbox characters not animating when typing/backspacing --- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 6 +++++- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 6 +++++- osu.Game/Overlays/Comments/CommentEditor.cs | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index e7699e5255..0c82a869f8 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -18,7 +18,11 @@ namespace osu.Game.Graphics.UserInterface { public class OsuPasswordTextBox : OsuTextBox, ISuppressKeyEventLogging { - protected override Drawable GetDrawableCharacter(char c) => new PasswordMaskChar(CalculatedTextSize); + protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer + { + AutoSizeAxes = Axes.Both, + Child = new PasswordMaskChar(CalculatedTextSize), + }; protected override bool AllowClipboardExport => false; diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 4abbf8db57..6f440d8138 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -63,7 +63,11 @@ namespace osu.Game.Graphics.UserInterface base.OnFocusLost(e); } - protected override Drawable GetDrawableCharacter(char c) => new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }; + protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer + { + AutoSizeAxes = Axes.Both, + Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }, + }; protected override Caret CreateCaret() => new OsuCaret { diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 2fa4cb68f3..7b4bf882dc 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -158,7 +158,11 @@ namespace osu.Game.Overlays.Comments Font = OsuFont.GetFont(weight: FontWeight.Regular), }; - protected override Drawable GetDrawableCharacter(char c) => new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }; + protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer + { + AutoSizeAxes = Axes.Both, + Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }, + }; } private class CommitButton : LoadingButton From b61e56cda519ffa4cb1c284e3134e36077be8a52 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 9 Mar 2020 13:29:17 +0900 Subject: [PATCH 163/387] Resolve post-merge issue --- osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index 608d98a16a..04fee8cd7d 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -33,6 +33,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentTeamScore.BindTo(score); } - private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; + private void scoreChanged(ValueChangedEvent score) => counter.Current = score.NewValue ?? 0; } } From 832e64cc958b7c427bc1cf0d58d0291d1990168a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 14:57:19 +0900 Subject: [PATCH 164/387] Fix test failures due to null current match --- .../Screens/Gameplay/Components/MatchHeader.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 7e9d0178d8..69a68c946b 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -92,17 +92,20 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components [BackgroundDependencyLoader] private void load(LadderInfo ladder) { - currentMatch.BindValueChanged(matchChanged); currentMatch.BindTo(ladder.CurrentMatch); + currentMatch.BindValueChanged(matchChanged, true); } private void matchChanged(ValueChangedEvent match) { currentTeamScore.UnbindBindings(); - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - currentTeam.UnbindBindings(); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + + if (match.NewValue != null) + { + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + } // team may change to same team, which means score is not in a good state. // thus we handle this manually. @@ -131,7 +134,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { InternalChildren = new Drawable[] { - teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value.PointsToWin), + teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), }; } } From 33f457d663bf6e433664655935dadf764691ea65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 15:08:24 +0900 Subject: [PATCH 165/387] Fix layout issues with TournamentBeatmapPanel --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index e09af06c89..8fa35003e7 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -50,8 +50,6 @@ namespace osu.Game.Tournament.Components currentMatch.BindValueChanged(matchChanged); currentMatch.BindTo(ladder.CurrentMatch); - CornerRadius = HEIGHT / 2; - CornerExponent = 2; Masking = true; AddRangeInternal(new Drawable[] @@ -72,14 +70,12 @@ namespace osu.Game.Tournament.Components AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Padding = new MarginPadding(vertical_padding), + Padding = new MarginPadding(15), Direction = FillDirection.Vertical, Children = new Drawable[] { new TournamentSpriteText { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, Text = new LocalisedString(( $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")), @@ -88,9 +84,6 @@ namespace osu.Game.Tournament.Components new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding(vertical_padding), Direction = FillDirection.Horizontal, Children = new Drawable[] { From 48c46efdd72ea4a8a34243ebbe3f3f56d37d3b05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 15:09:34 +0900 Subject: [PATCH 166/387] Remove rogue newline --- osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 1e20687a87..34fa7a4997 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -35,4 +35,3 @@ namespace osu.Game.Tournament.Tests.Screens } } } - From 6421f28ac70964d3438aae62cf0df9888c4ea4f0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 9 Mar 2020 19:07:44 +0900 Subject: [PATCH 167/387] Fix nullref --- osu.Game.Tournament/Components/TourneyVideo.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 687acaa6d5..43088d6b92 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -64,7 +64,11 @@ namespace osu.Game.Tournament.Components } } - public void Reset() => manualClock.CurrentTime = 0; + public void Reset() + { + if (manualClock != null) + manualClock.CurrentTime = 0; + } protected override void Update() { From 19ce2d643e39726119d570d008e2af277deebd58 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 09:51:30 +0900 Subject: [PATCH 168/387] Remove unused using --- .../Screens/Ladder/Components/DrawableTournamentMatch.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index 18ff8abf61..655beb4bdd 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Graphics; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; From e7f1f0f38b50946a147828699265e35c6ad9ccaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 10:07:47 +0900 Subject: [PATCH 169/387] Fix hyperdash not initiating correctly when juice streams are present --- .../TestSceneHyperDash.cs | 52 ++++++++++++++++--- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 ++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index da36673930..7a7c3f4103 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -1,9 +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 System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests @@ -22,8 +26,17 @@ namespace osu.Game.Rulesets.Catch.Tests public void TestHyperDash() { AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); + AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); + + for (int i = 0; i < 2; i++) + { + AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); + AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); + } } + private CatcherArea.Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap @@ -35,17 +48,40 @@ namespace osu.Game.Rulesets.Catch.Tests } }; - // Should produce a hyper-dash - beatmap.HitObjects.Add(new Fruit { StartTime = 816, X = 308 / 512f, NewCombo = true }); - beatmap.HitObjects.Add(new Fruit { StartTime = 1008, X = 56 / 512f, }); + // Should produce a hyper-dash (edge case test) + beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 308 / 512f, NewCombo = true }); + beatmap.HitObjects.Add(new JuiceStream { StartTime = 2008, X = 56 / 512f, }); - for (int i = 0; i < 512; i++) - { - if (i % 5 < 3) - beatmap.HitObjects.Add(new Fruit { X = i % 10 < 5 ? 0.02f : 0.98f, StartTime = 2000 + i * 100, NewCombo = i % 8 == 0 }); - } + double startTime = 3000; + + const float left_x = 0.02f; + const float right_x = 0.98f; + + createObjects(() => new Fruit(), left_x); + createObjects(() => new JuiceStream(), right_x); + createObjects(() => new JuiceStream(), left_x); + createObjects(() => new Fruit(), right_x); + createObjects(() => new Fruit(), left_x); + createObjects(() => new Fruit(), right_x); + createObjects(() => new JuiceStream(), left_x); return beatmap; + + void createObjects(Func createObject, float x) + { + const float spacing = 140; + + for (int i = 0; i < 3; i++) + { + var hitObject = createObject(); + hitObject.X = x; + hitObject.StartTime = startTime + i * spacing; + + beatmap.HitObjects.Add(hitObject); + } + + startTime += 700; + } } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index b977d46611..71228f1c07 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -270,6 +270,10 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition >= catcherPosition - halfCatchWidth && catchObjectPosition <= catcherPosition + halfCatchWidth; + // only update hyperdash state if we are catching a fruit. + // exceptions are Droplets and JuiceStreams. + if (!(fruit is Fruit)) return validCatch; + if (validCatch && fruit.HyperDash) { var target = fruit.HyperDashTarget; From 059af2a9866ff15d6c1200b1eecdc6b597267b5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 10:43:06 +0900 Subject: [PATCH 170/387] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 97f7a7edb1..6a8e66ee6a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 855bda3679..cc1ab654ab 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index e2c4c09047..04b688cfa3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From e6858bf1304f0a96e1f3ead46640b0b83fc72d4e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 11:58:33 +0900 Subject: [PATCH 171/387] Fix crashes on some storyboards --- .../Formats/LegacyStoryboardDecoder.cs | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 6569f76b2d..c81f933bca 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using osuTK; using osuTK.Graphics; @@ -93,8 +92,8 @@ namespace osu.Game.Beatmaps.Formats var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); var path = CleanFilename(split[3]); - var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); - var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); + var x = Parsing.ParseFloat(split[4], Parsing.MAX_COORDINATE_VALUE); + var y = Parsing.ParseFloat(split[5], Parsing.MAX_COORDINATE_VALUE); storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); storyboard.GetLayer(layer).Add(storyboardSprite); break; @@ -105,10 +104,10 @@ namespace osu.Game.Beatmaps.Formats var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); var path = CleanFilename(split[3]); - var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); - var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); - var frameCount = int.Parse(split[6]); - var frameDelay = double.Parse(split[7], NumberFormatInfo.InvariantInfo); + var x = Parsing.ParseFloat(split[4], Parsing.MAX_COORDINATE_VALUE); + var y = Parsing.ParseFloat(split[5], Parsing.MAX_COORDINATE_VALUE); + var frameCount = Parsing.ParseInt(split[6]); + var frameDelay = Parsing.ParseDouble(split[7]); var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever; storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType); storyboard.GetLayer(layer).Add(storyboardSprite); @@ -117,10 +116,10 @@ namespace osu.Game.Beatmaps.Formats case LegacyEventType.Sample: { - var time = double.Parse(split[1], CultureInfo.InvariantCulture); + var time = Parsing.ParseDouble(split[1]); var layer = parseLayer(split[2]); var path = CleanFilename(split[3]); - var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; + var volume = split.Length > 4 ? Parsing.ParseFloat(split[4]) : 100; storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(path, time, (int)volume)); break; } @@ -138,17 +137,17 @@ namespace osu.Game.Beatmaps.Formats case "T": { var triggerName = split[1]; - var startTime = split.Length > 2 ? double.Parse(split[2], CultureInfo.InvariantCulture) : double.MinValue; - var endTime = split.Length > 3 ? double.Parse(split[3], CultureInfo.InvariantCulture) : double.MaxValue; - var groupNumber = split.Length > 4 ? int.Parse(split[4]) : 0; + var startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue; + var endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue; + var groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0; timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); break; } case "L": { - var startTime = double.Parse(split[1], CultureInfo.InvariantCulture); - var loopCount = int.Parse(split[2]); + var startTime = Parsing.ParseDouble(split[1]); + var loopCount = Parsing.ParseInt(split[2]); timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); break; } @@ -158,52 +157,52 @@ namespace osu.Game.Beatmaps.Formats if (string.IsNullOrEmpty(split[3])) split[3] = split[2]; - var easing = (Easing)int.Parse(split[1]); - var startTime = double.Parse(split[2], CultureInfo.InvariantCulture); - var endTime = double.Parse(split[3], CultureInfo.InvariantCulture); + var easing = (Easing)Parsing.ParseInt(split[1]); + var startTime = Parsing.ParseDouble(split[2]); + var endTime = Parsing.ParseDouble(split[3]); switch (commandType) { case "F": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); break; } case "S": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.Scale.Add(easing, startTime, endTime, startValue, endValue); break; } case "V": { - var startX = float.Parse(split[4], CultureInfo.InvariantCulture); - var startY = float.Parse(split[5], CultureInfo.InvariantCulture); - var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; - var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + var startX = Parsing.ParseFloat(split[4]); + var startY = Parsing.ParseFloat(split[5]); + var endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; + var endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; timelineGroup?.VectorScale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); break; } case "R": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue)); break; } case "M": { - var startX = float.Parse(split[4], CultureInfo.InvariantCulture); - var startY = float.Parse(split[5], CultureInfo.InvariantCulture); - var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; - var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + var startX = Parsing.ParseFloat(split[4]); + var startY = Parsing.ParseFloat(split[5]); + var endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; + var endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); break; @@ -211,28 +210,28 @@ namespace osu.Game.Beatmaps.Formats case "MX": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); break; } case "MY": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); break; } case "C": { - var startRed = float.Parse(split[4], CultureInfo.InvariantCulture); - var startGreen = float.Parse(split[5], CultureInfo.InvariantCulture); - var startBlue = float.Parse(split[6], CultureInfo.InvariantCulture); - var endRed = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startRed; - var endGreen = split.Length > 8 ? float.Parse(split[8], CultureInfo.InvariantCulture) : startGreen; - var endBlue = split.Length > 9 ? float.Parse(split[9], CultureInfo.InvariantCulture) : startBlue; + var startRed = Parsing.ParseFloat(split[4]); + var startGreen = Parsing.ParseFloat(split[5]); + var startBlue = Parsing.ParseFloat(split[6]); + var endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed; + var endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen; + var endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue; timelineGroup?.Colour.Add(easing, startTime, endTime, new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); From 7a9c85d69da0a826ac03fd78fb615e146a0eef96 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 12:21:40 +0900 Subject: [PATCH 172/387] Fix now failing test due to parsing ranges --- osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs | 2 +- osu.Game.Tests/Resources/variable-with-suffix.osb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 96ff6b81e3..76b76aa357 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var storyboard = decoder.Decode(stream); StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); - Assert.AreEqual(123456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X); + Assert.AreEqual(3456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X); } } } diff --git a/osu.Game.Tests/Resources/variable-with-suffix.osb b/osu.Game.Tests/Resources/variable-with-suffix.osb index 5c9b46ca98..fd284eb055 100644 --- a/osu.Game.Tests/Resources/variable-with-suffix.osb +++ b/osu.Game.Tests/Resources/variable-with-suffix.osb @@ -1,5 +1,5 @@ [Variables] -$var=1234 +$var=34 [Events] Sprite,Background,TopCentre,"img.jpg",$var56,240 From 0d18ea1d2955dd76832d6d75381bfc9ba0994105 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 11:42:08 +0900 Subject: [PATCH 173/387] Add animation and fallback catcher support --- .../special-skin/fruit-catcher-idle-0@2x.png | Bin 0 -> 57593 bytes .../special-skin/fruit-catcher-idle-1@2x.png | Bin 0 -> 57011 bytes .../special-skin/fruit-catcher-idle-2@2x.png | Bin 0 -> 57264 bytes .../special-skin/fruit-catcher-idle-3@2x.png | Bin 0 -> 57077 bytes .../special-skin/fruit-catcher-idle-4@2x.png | Bin 0 -> 56155 bytes .../special-skin/fruit-catcher-idle-5@2x.png | Bin 0 -> 57143 bytes .../special-skin/fruit-catcher-idle.png | Bin 133664 -> 0 bytes .../TestSceneCatcher.cs | 1 + .../CatchSkinComponents.cs | 3 ++- .../Skinning/CatchLegacySkinTransformer.cs | 4 +++ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 10 +++++--- osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 24 ++++++++++-------- 12 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png delete mode 100755 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..786e5cc25ad2e175251acdee06825f936c8c9ea7 GIT binary patch literal 57593 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

~&^CXCgh=e9m>w~R$x}C+NMpVTm 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..953a04d4e4f9cdde644d6ac6e9715b6256631438 GIT binary patch literal 57077 zcmV)4K+3;~P)ZX zzp2&0yx+yT-NmWZz?{&#yx_;M*uk67yqeFuzu?8$>&d*|#r|J z>GaI+_s!Mh!MNVUmeIVW*TJdQzoyZ>t=Gbs(!QC|ysFs2tJ%Yu(7K}3z?#&(o7KRU z&bY_o$j0BtsMEfk)xM?Gy}aDSsMNro*TA6AyvgIrmd?Ax-o=&7x47HGwc5hE;Kse+ z$g$hOuh_qm&AF@8z_i%Fj>)>D&$^M!y{Xp2na#M5$+f}c%d_6Y!`#En)+= z&&24?*6YWN&%LqM!nN4FkjJvw@Y2xe#Kz^y&+5><*T1vWyYBeZ;Oxlp_{qTB!ko#p z=kd$n^UmJr!I#s-jl{9R<;S|-%)a5n`S{A~_0F*2$idpam&&!O(Y@XE)X(C=$>PA~ z_tc`^$ocWY)#b#-+`hl%()at*fy1%B=gaN$!?@_z$LiG2@zm$*zp>)c;`rQ|*vi@A zzSQ;Hh{3Sr^TYG%zmL?!!|=}eMKW9oA%Z{!oZoq-wJxi+E()Jo zwjhLn5a$g5h!LV=tVeqhHy3j|H`jc-IW&*%ZT@=cPAAlcjdbPEu+5FpU$Ku!+eLAb zr0GJ2!^0GR(R!C{C#qPFiXWADD4ucpzdEV!J-j^SML1AwWHP#snbq3{dsI@m_^>}P;KG(p( z0hd`}GibRlve_5hT zn`cnCc;oWrONTzi+vsRG?23mdO75rpCe8V;|N6~0-~87%-~8)ezP@pFw)4dC;}=eh zjEr>8Ub=q$)8ofijvhSalPjnpiE`#hQk)JJ6&10Iq`@Gc&MyiU?k(9{SlHg){&@Rk z^@ML>YHDF&AsqJ2`FuW$#pkP!)yoIfYK>a$?WL*I6V~2dt@i%?dk-JpzIf}}ty_2Q z4&A*wH5J<0`gV924_lAcoK7c@F_IN-?&jv7H#bvyZn0Y?lR0EbyPan*;>jEp#cu-H z55#`;*4qJ9pl&N*P#Ly*dxv|io(Ye|GB+?_@r7+RO_alVk>B2YvYE?0={W{so14!o zyZQtLyrLlAonUT*)(lgZ&wITzwNhg18MFx%i#LoeR}P;!bLQ~q(#UN8?B58mGZ#Z$RC#zd0CMR3(SKsfw z4`TNp-oAx;aK}6~G&MBz?bg;-s1*;-o;jU5=c7k!Pq^uHaV2XrH^XF#r;`qcBjHFU zc}8Q9M|T(N-+t>?KPL9(+wbVt^(_62u2*R=1cpN)k0*&6f_zumD|qe8M!A< zHmlWyTS{Ni)z{a>c`^I_&H^(7NH`p zpeXa?q_yX2{ne{qT>bj$)qD2{ua%Y2k%P_M{r#6f48YF%d|_xzD6~2^9BKN7y+|tE zpwk<&FCWY7(AeI>Cg6JMt)2|yE6r2k1$;=BzEiC zgS+2`hESna=xZ3_AjsC7YtNpo&xqF@8|&*bvCQE((l?!)?wUU0a3mcYGX4f9_s-jI zzDDf*igl)l$(&gWpgS8gN{z%b=yJ{Z<`&F04>XnE=5V?B&3uZsnMKK{fEHy5O4oi4_`^_=G|{mp^&w8*r^*{+X~dx zZEdZsf!HHadOevu;+O`h>Ao%^h_0@r!w8YP)7;$qKOy!Wvw|rSiDqVIq5*?MsgzhH zepmCr0Hp5ssPZUIC3icYLa|aQ=Jaw^OQ`_Bcmm@F<77TnEFvzZQqu5GGj)N?;*Bpa z|Lr>J&^(Cwc8F1fim=~5F*!IlaQpU^D;F-DL0wzDI668Ch4o*){weT{zzLwRMypM! zWw^5(exiZSm&y1EQZh$TJL>eymtNBjFLPU)LI|OS=LY8HP7Sy$E?0eX=ast;@7%fb z@D3sMzr-GpmW0IMUJzGnZvb`=Vk#PB1}|H4R#a>lyZXk#X?(e_rEk2iZ+!d+l$Ml< z7`fv2Kr>zDqnv#~0^wudOH*OG#9hxWA@3RQuOF~FYI59DC@7_HSyRbrd zt*ovd9T^+zpPjvQi70I3ti=*mP-biEM21!Dre_=Me3U&QOC&ZNNxKvdcKW2(tM>bd zyyEdTN-hkC=X?M?0AkH9*TE|n?vT2B_uIR7@7}q4=OOsrd_d|B{A;)_5HJ`F&b2hU zF`!ayeNXIJg}roqdAYBzZyArv;{Y~3jfbuzlaoun@%CFk9+rRq*4w+@PjeX}7Wyq3 zH8eIV8$%MCS>D~>dcA!M!PtXJhD;DVzIQ^Y85or ztJc&gRheU?DdAw<{g*(@Wl>N*MOZO7I5;;o_YlBtUs<_z4Yjg5a`f!MenRXzfQ=p; zfH$Fmve0-wuZN0{LV*dV;6`mkE51>ZXC>(zx!^y??^E# z_m8_k>_>SF>0;NroE(dRhSNV+85#`+iB;o~)X&ZyJT)*dppekCP6j8(pmSI`?hCrH zuePOQ=ezTa8yg#mqzKB>Y6CQtN=;K{Oqr+W&fT~?e`vnH-xceJg2uvO(mVdadfy~E zcSvq_0?Af!;vHsb4c-qKFvjyHQ7gA=A&7XoN};It%Rwq^X>M*N5^L_fu(EnLM5f?+^2oDiVD<_cv9PTp&j4p@%^7{3 z;w0>Jx=7?)>jg%VViQIpD?;qm&(_uV9cpV=_=ZGqp4AG2Jr_&l} zG^HkE(`e3pd82=Rexy6rBkvv?>FgW`o6QO#HRun|!Gpj0>h{rIt-|tME?4u} z7@`{_hL;B_>5j>TLgAoC%Mhp2?gnWAo7cq)@{GKe@{)ogu{Pv4FD#fXW`$Yd_e?4j zLb<%2>ULRt7T5?RcI&~n%=IJ`X`G%uQt@buNNvaQpRKPygPL?}0X=IoFK#K~yVGj{ z(v?7#-Y}vc@nbFH4@ePe-z_gsi(pmnzt)r>29fQ4M~^A^WF|^eXEL;qMPgE z9z8o3rdoA)!NmzX;z`2JF4$96HeOzS;Im~xBF$v5m}wDXhUs+b15uSSW3!oh5MR-Z z-Ld+f%a_K+Iy-kd%?z`ioV$1XtJ}AiR&TAYtS+4jH(OrKx7qnmj}zCoknWO0WYmO4 zrJE6P=viryU9zWaPl-SvV3!tPN`|IT(3r)ih+BsUT?!hrfEO~#=!KPAHy=41JV8rK zZA(|F5$>I=n0fZ>$U3Qt_4P;2HD@)}mly5XcDh)chMTQ{m^g_p{BiYlfLmWuq&Gx& zzb3I)-oNWZ7Ay56rPrxcy((HLBaxVTK0k4I^ypa(FRMY9W+Y_6U}-6jw`cF(lCrAu zj?X?jz)K*K=tad04olD6nY1c{L=!Rf-1vO9yScu{wD|e#*jV?hLO3`$;m0l!4};jz zi>nXtwsZ=~KtahZvHETzv7@I5v2fTzA?(IIDklaJi=UOU+3b=M_MSZ@C53yXQs*Ra znWxM?Gx=Ht6{aYmoT7G!b&ri5K6C5lkce*=l+{+%wzO0MT8q(WOdc^FnFciGI(DKp zofpw^Gy7O7l_tW9Ukd`s_QXjEn$CZ2Sl_7gc?@>xcaMBPSz$nL;{^Y7HtH zjc6r8h+QB*hcB)}p9irsU_1&0c**g6I#AR0gjmdAa2X5+GtCe&_0ecRf-MQ8V)cVP zJ%CP#b;H8^wh0iMwD}G$ff$S!3R6&1W=bxP#ro$D9Vh>j4;0McWsX}VIuK*A+-_-M zp_C^r+zVp(Th=?V;0uRgWD3X(cu7~*%c&iSjUlpJyg8&#@C7YpEwyE3oZaHb{$tiDbG$3~UKrZ54nKTCe<{Jhr5@Ue#~kc>8sU?Ks(+ zyWaR6!UIdIQ>oR62@w;~*oBKrr%qYI@xY_Cji4R4*hpa|drDhs@qF;&+3i6(hrG2B(PmvZy1IOFq605b&0K4JP`O>DQ_F#KcA-ja6{gV^uK#+p#Qxx3MBBVg9o`S%- zk%?C>E=>iP6=GvoOI2Cf{{7`;_)t|%?~Ya`#89)EWjJjSRe+$hpPO4-;Z@;VEnHnv==5 zgH|oxfvu%<51Vbz^7%n~P|OvHFsYd&&*f4EWyYg1_4N2{rbuM*MmLC!#3nV7h(;1Y zznTVJXP1@$Y$1#!)h4H6-LdA*`9p^fUp(l7hxv%Y+N`a;szwmgXR&=FwMwOUFKRD5 z{n86q#KO?GatdMJ0?&iSgw6e(onu!nt}ZPYoHJ{z1bVEh>cIX__LY}?vcG)azLKh{ zQUNA=I?bHXdUe&+)lWA6JA3*xkV)I=ZZY2hVjbnLfR(rOwUm_hHDvQO>W_B6){{S& zKSB1pl(SmdNIOqEU!#T{H6K0$Vhi{Qv?9*T#x#1emIp(DtrP}%!N+_Vt{3(V5vP(< z$;ydRsa$^3pn;qsn#CTt+4*yqXXinzc}Rj!J(XyH(li)8wFF@1DUuB+3UR-?bN<9d zj2M>%3ZsTXZ6R#78N*gI#}tcYMF_2^)2C1W_4H|P*aE6#kWhXhfcad_77A@0b3tK9 zVmF5njSRZydKs?@06zZsUnKm0@-gX;bh7TO|5zQ)U?=nm<+VD<{^VlR4kU2 zPGc+T60q5O01T&|mwbCyL3;z8!$?;!^$c>;*v!#tOd1cM^_XhT_nf;tHhN-yq&}1} z*(6GYH3_1J#CPiE0%dN)^pwXeF(8{A866qvJV;T3S+GzW)GfKkDO8 z_G1vhMjUB|9-&|7)FBu&NVBJ#g6t+pjn`gK+SgIuLhK6yWAo|sESIHyAHaTm==mwK zH~vMhr700Go*p}9x;%dz#BN%KhEyz3WjbLu8riix0h=c%2ug!?X;u&{WlQ-CjtIr! z5@I>Mx<&&kJw2MIY7-iJ_Ryix`Ir@fRHDSaN|VW`8?vH}<-y1sy2T7@rxfpys(4!YZ2 zjN8L|Z@dodr;Yi>dnz@_p~oWUdiqbCLAY5k4~39zE18Leok)xrSphGYum`0ADK<+n zmkTlIxfpEN(-He6nM}qM$*5~0k#nD4!hvkw;(@XxG)PyCWZdZSv`)T!XsHIbdRERq&BH3ggY7P8?s zWi6%9mpj2===B@``=KR&2HEd_qgK(5ojZ3f)_M3iAvV+)g{4wFbd^BB_zC1Qe$fAO_gYWIy;{Vz+O9 zH8)2%Vgge_s!gJkVO0RJ{reA;V__&0kW+4uE@r^^4FG0PDQkLayxWf-KW=D8U!oeM zb_X5psMl^jRi#qz??w)L1?=aM5&!@2HyCuWe&q0Lt2e2(x@aBRl2Ke)$w%tPw#G}) zE2sNvdBGxHkj@uZlBk)_rEtGcscjmQClk?-AfbtTi8yv*biUiFi9kUaBp0o=GPjAI zFg#Uda1`|0fYycy8u23FgSpNLnGIuEUVF4oHX3d3WPE5y(H8eB;&FvPj$RhVg=ANE zVe=*qhUL2xtLnm3-0YXTd=!PWq3{?I!QkGqa!kt4_L12>jqE6saneku4hdbQR2h`& z^P1<}?Bi?^kl{{=shMED;IN`m)l-`4Hy=V^$YDPZ49LFs|J{Gkm}0XhaN+|on;{y_ z<+wQ!v$8S~G#0@P+zAI9vZ#o>a0ziSCzs0UwJekuR12mn{-O1J_iGAY%r@>6}pp$SGO(Xnm(8LsMi<*ld0q z&cZg(Gi!!Dayj%Qw{-WTI?34hY9Z8XwL!3QA%-wT`J*zvg8wz<$`OqfGn1vf(k_ABQHv`j;u+#ta)YbKubFh!I#%lGQ<{c$-UOe|U}Q!# z#QA}248&mmw9OQY`{METVoy&0;?Jzyb3jGh$V7Tf=hfQcVosicGw`DwBg5@|4Zxhv z-pPe21=$%UtrFU#wXJQ)V};1%athgE=lq8A%D}=q2DFCI5E@Y=Ikj<(tx#Ub19!MnS6{ra_z z{Kd}vz#7z|ME!g_Um9tWwph1NPlU84=3CR0I(Lp#%0 zfc2=O{F9EVJ|sxTMncRe7IU;ZT{X=>12F@vUz0JciMb3e9qEhCz?OFPm39@_jgAd*K9%F>^JD@w zsv4CxC`?15YR#YjLWqry^-oDOYFf2cyUopJ8)T2O{OpSqE2l)%CBQrxn~A0qaq}V$ zrRUA0!#Itg(p)yj-DL^B}QDPUz3;ux}AefR94>$nwsH8<3&)f$ul z31m3Lc_wL$;GSCee)%Wm`^HIon#TuGgUY z=tDFl1om1-{*ugyn~`53|LNB1LgTS$4hoy$azTvy!fmt%?Sj7XDs1|FUBMuZutts^ zE4;T_MN^Weg3K++cp#gabN_kcZy?s+AGT&_2He;Hti4E<&GIW#xs*z2LupVTr_^Dg zXL2~{3dXawfD;lERjjXR2{Nk$#8ie*NJ&#-2Ak1#aCa<|^*DKaN*1|=5Y`?Vx{?r6 z-~c*VT^tmY{l~s?q>Fuhr9pmz$zX_a9Q1n0-V9zebY@1W(ZI;W?kru*;h;lQ-}Cwz z%m3NdeD!Ph4;r5bqHuO*<%`Nn?u$H^94w3i03(-OqXWdc(o7K5s%YfsfjEZJL~Jz~ zoMJDN1ly0}g;h-hF;0WD$qr(P1j4Q%tjf+{qu(pqYZ8j&2WlyG@G9ih5=FmVmcYGw505lkM|$w zXfeu=-$j*N1}l$WsKWMD20T+3|D+^C(*l^AkNXP)y8Gbw-}8dN{)e`=@ow_G(tZaB zPzZ+xoX`-G%p_?m>syv&BTKT8Em@LfZ24P~ZLlmy*v1YDUjf^E1Plqd5CRkj2;rqP z1Zcj8oFO^QFz@NCwzFndj0M;tvXQOoA34AMnCZ-Vn{-YI+_4R0H54xIz3=^T?dt}G zzYVhg#O%4b%kq{;9Md{6U#JAx+QLXOv7@ktj^e)q+3g7%qkdy1K!gA@RjR)}6!H%C zGsh2I`tr-OlJoC75cY;DvUYpNHmkKb8D&PV&idEKX_`@g)(?j3JvYCx+3dG9z!j|K z4lo7PrCKdUvB!s<`T1*4^mV+ccq)$Z z3^26(_2DB&kE}b{tn&Cih4%Tre?#AK3_-WEc1kQOv7AMlkoD82IYT zkAD4Yh!;&%(G5m>#A0wItRn45LP3 znaxUw5H^~_1P+`M!}n3`MUu2%8kRmkXTcA^>%SArz$}l*9xw(# zEE`nAwV{n(_xAhmzh9Ts*{Z7pX97~J5jbNpM{2e|rV@J1#mirPL~K5CYX8aRzE-2X zm}1vLBelbvp4R!@VcI>t@m_NN{+pT%?Ga}Z$kb7edQ=Ohn5nxmNj|t&V>fqLyKBhDT%~3;%%e~RNAegca6=9N zQ@Q)%wCBwwl7t&gJufU&QWtxIY2w>fnhT*n!pdk!;* z3$0nGRQ(~ZKo+_oh#eNhhWp;QX*bJM$jUdrL_sXoOAKF)VT9}5t){F_I~pxbnxk^C zv>@SHEa0=%mF;#6&=vxxas&(8VF`|-gIjnSNr;3-)~ zI$2VZ=xD)Ox?TE~pblpWV0@eK9GXs>`+%%BoKg_$zj*lzLG1I--+kweo5=`((W@_j z(bSoqHtk7~Y!azKEbP%`vL zlL_ZTph6{P4!vHxGaC`9Gf<{?jcNohMg!cYdLP=}?E!H13W!mJO{bYa}Q0P(F>8 z$`u-gx21JLP8lVz8FN`9X~-lgG%o4G%A}dZg?XvIXrICaI&z??m()0ro!kRp;1LFr zVO47~i9L7k-d*Ve4=s~e6&S@*gJHTVCr>&Yea$B&6?Cior!C3bNsaI16E89@dRPb7 zT*>9kcY9`h)Xa#YxZ`}HysJ`Q;55;&hSmd?Jq@jH{BrM~-spq3ahDeL-4g>J-uURv zBcH!^t}z@q-BzRTq-O;I%uHLYR7s#HcHop9A3KvVj!v7CM)hbqnGOVmJ1#_7nDWL~?|TzE<8Ll(XzKO&8fFIWy2JhT?eXTg6kaO(om&2% zDr+jcTnW2|z^~oAzRE-T+lNz9qNd`+j^9 zhF&8xq*~Ze1ufND=9Ca23309dP3t}GzP{$oq|$H+QmVT%AAWWA)S(OSZY*=y3|5z` zvr}&>K28cjJ{VDj!=jH=?U zxBpVe%SG%+)z=>%+He-9{9lJ40`;nVzC9{8H%RYL99Ij+W4=gXt!u4|8Ir5EHs~}_ z!$UwSYJFP>4h?%xO@4Fv+o^xcWcpbY{JzEyU?Py=$UL6B+gHnvg4AF z(d!@dj@UG|D8*e8$kJ+^7WUgjQt5{0#&w>_yM#qa+vp61Ng#U3VB2>dRrriWl-+J3 zXeb;mTU|87pp%=hO2Z$mvZhAHl(nVy2@UattDHJAh7t0ghleDbQO|l>@6_?#n;G19 z1gCtcHnwmjv0obvnoFe2Ppi}1MUXlR%L__x6Zt_ba7KG`QWHDGQ+?G~-qJ!TgL5K~%|3_*)Xw}#vKx(>vIchFnb>+pdFY!9i!8{=`3h&?S`mCVDjFhk_dgpKLZ z86EWkZ4yUVn!|7DXk*gi^MROKr5fxH-$$%$cnx2hHXMa_@Cm$Thu>!NPrBQ>O%{M%tso=*1IH^4Z(S^?W?+RXJ4tg1U~;djnU`{%pbD zJ>;|KX_GA(RhzJ{U^*)PgO!y>BBx=_=(sH9(~noWy)iN$5R;0$|I+adGX807_C&Ft ze2c|uG3dKo%pe$at2Kct6^PY)tE;1<9i?28rT8ZQF99b!J|ktN->2j(?uvTl3`sgN ze2{8;&4GQV4*%u?1DH+J9R7-UJ+xTqS0DHIJu@v`H8tB`UaL1ml2NC|Wb}K)B_W*l z!HOg3_YQ`S*Lw+To}1-`+Z4oJ!YLo_!7WW{wG5Q-C)zatkSvXuOHbEEZ6iSfnZydp zn8|*oA3}6dNx|eTTGw=rsjZ_TlT3CbEcy9*SE4fC3YEfIOw&{6sesPXbH|=0h9&P1 z_B)C@)U?6vw_cxshXZk(;0R(~f+Ly3@Rlu&We4Hx3y3BjIu9sFr>7l* zRk<6Ir(NB26Z7>gNv=t)ZBIoA#7Og-A*_>sga@%Shc~r;eRX-Fqhoa7>Q5j4yN7)q zo0m5=Jw0k3<;&>wNNEzp=q8p%)Kj!lXlcyAZD;7PC#0I36s}@Ls3OI%V8e!{Uma|8 z#Qf$0ZAU@^e_MIEo|!7k0Qfh9C7SKOAhW-HbHu$AtxJrYR3z0|#`_5RzZlj^AtZ(Mz8{zFIXM~j|XB6s98`!?#R@{hVS$%#5? z{ZS5NlqM{b&OKw3LWyPeKuSp)>)g4<#?5X|)S!;S1S2FDc2Qhfiwi8Ux01PBPo|n$ z5+_eYS|SMsducL%JIRF;w)`1tN`iJwYC^}!GHlEdA6~sxSa$F3diUhy9xZM2+nO^T z`Wd~AMUoDuJUVBK!K$z9)_11UG>)=k^oW`%_`Q;=apRs0A18{%9V$<2OKm#+;n!b0 z`#5p`VW0UwS&>fDQw*Aj{G&9Uqvk}hSq)?p+G=M&!WbP4&)pMbXRw2?FRQTol$SaKcLj|t@I0wE+-GbQCi*tQWqQ<@&CG-_ zcM1{m-ey58PRbUS^e5%fv_wWnzP|CqBQP-D2F<7iFsV+gR*g}4SQ4te~ixC1!ODVsY zb!l6ddnlQsb%KLuKNY0B-hmzt-(zH#H($NvWhd$1aP;oQ8a8Y9b0 zk&dY)#eindovC!QY?>U7NeP(MNin7J(72NXoqYuMvNmpMcEkcvb)ux$01wzD?6VRg znF}gff|Cw*m=nN8AOo_ff>=vXW8*lFQ9bl54qQ-$E08d5fwH{a5YYX4rUI}6z zuEOTV-lC!K$%=+d&XlNZV*s5tLAz8@h4u?%Vq*6v3)mOEv8vXIWcuFa8-sr>-esw>^bjORb~|F5 z%t}C1SPs@9dgJ8$JtMgYOioL2Fq50<3DJ5T98|EPmc+QkKfTh+ zP?^l;>922m@%`EBV}SL1Ufwf{SIm9v!Sep8AR)|T5{6ycj8R}|%&pRdeKd(0m(6KLa%?V*jp?AC-E~vnRrG)L)s?fS zKL3B4)-@jNQ+XTkIyjnGn1Pev4uDb9R;W)H@YV#oM`{qG!COLhYmNq|GE-CO7L$gf z%e4BH`&Sw7g$EgnKz*-Q*$edDv`}btXfqxaJ=k`sAW{_#J(0Hj8huF+k|Ei&6d(@H z$&>XM8P&QMz})Uh|J1z?FW~vj9-0l3FEV^%5#rOMpF`KA3l?{HG)&atw?T2_ zSglfFLJ`$l3DS?=mW=3rth?z|}Uqi|hy5yzm7PqV?mK~QSte1lQ?%3FeAAWfG zi=VzY|1XVW;hdMAeS&4z;o;_(PhD)te+IXFpq>w06{pE)i;`k+^_4KXgsMm_YszFb zxfx*8NP!g^ zWU)D+4#JZt&1fftl5CUV2rV)WzDetXfnNW0`{{B)tkat42padehvi1-?X6e2{UGLw z*mDgHs!)~O+A8Tal2|nJgY?F{sS^VOmoI;K`72E9M}L9Wb1yyn^LI&$8XApe97fJ@83OVQ~pL2(`L!xwOJ7uw&DbWMuqY zn1sg=RIRJ+;fjXaJG$~ERnZ%$>oe{+H)s(9J-54|w9?RGHww2gL>&(eOS(JEll$n< z0v^}p2>`qNuU~!nmnQb3=U)2hzr1w;`pZTT!+itEXP#L;Z~lWN{>z;?Eq@EX`=kJ7 zSjvRz==3zrER7bI93zYY+lkP{6DRsXOeVY#Bbs1C498iREXH<9I998*pu9YvW`=<{ zl&}h7WM^0#h!p@UZC;8cS*gwhw3?j8q?0v8e}hCaY-WhX>Xgb6UTg?wBW88?h0?060W$OZX#7jOqa1%)q_N|B8uS{RGUX^EIL$cR zG7i*^9ykD@5Q$lQvnOPV6wq#~EX2q<)l*rOm)-`}OvF6H_OgofEtTS$6p|W%dFy-Q z#|dZ0nW)2Q#v}j!UoYQy`nkV=V!=x?2Yu_pyXR=TEPdkHr{>Oq&;Ouj!SgzuO{>Am z($OS=W>gJLC#{{}veYqD0<%4119vCyPF}onaey$!6c)brg zK+URj)#$CVeKRuBM=lA_JyrC%#sWkY51BVT0E#{q8v0KHefy8PjXe}DO_ z?*I(1{3+|QAH20|f726BJ}--cKd^-X%gbamkyeBugXiqliHS6PLV5 z6cZDZ5gCiQP4uTXy0zrH(GF#0hQ1H9ZM`c$A4`K$Nr(1z?u4$od{PV)2dF2^rFZNa zEz6r!6j^%pnVGwHNm|I(RVfMpKX-2-8&df)G)|kQ##Qkx@cerR2Xh*3&iWx5Ktp7@ ze1UAsU=+w=bP~gZmk45iFX!p!7Ji5IEcn^s!%VY;1MSc;*ezrAhgR;z{{I5(JI&(74-S9u!B2no;+%!^A3{OCU|C+k zq@IY%A?$`U+wz}Wkj)r&`W(>Q)#?Z|Xi&nLEPAD4 z5}?WpWe1U#b{Aaka#=wyVN|VF=U_ET&>vGy1^GIYT09fPpgWrB9)US#SwdGJ2eX#q z(p_cJ+-$9K_W}}`?oQd`kt*Tpfh+A-u3VG@U~*pk9zdS^>2H4a(sQy)|1b{srOlWw zrMXPsR1gbhbpf~_CXI1?hC>h=`!iyU$P^oc7R`0p8+TXjaVDv{4TZ|aq*#ZmGGC8# z(gMQRT&_Z}%$F%up(O%h#vJzvhg+C45EA|u>4J)ImNj-H4E!c^+(?P(9d#J95D?=j z_aw`|-)!>4Au+-%KD&>Dp^@}n{q*XWS1w)#uMZW(=6nw%3txQhao~DbXTfrNn#(ad zI-PDw!|e;^Ogg7VN1n#8U;b(*IPBMm3=KOJA}}B;+)WI6yilT&x6^7eLJ~32f9PU! z)Md>#6cWx1g@RZiMpIZWtUk8*Agt+Rd57XlJpP1A_&KVO(A;~BW>RtR2C{s;feODw z9fUqX%9c%b*lqr*IMM)c%sx6RXCGW3K0f{Q3V_`|HvTN)wpBDE;YUt3pKSxDCZ zx)@4+7x#gSxrwEqu%LKbWSd<*lFd@sljkWmM)d_CDOEikd!Nb7ytrm>E6+m4eh;MsIltrl)7dY-{PI5$duridcPBZI&FW??5Gknn z59pPvZ1!kr2XhVeG`uSfZ7fqJ$0pg-jQ9@r1zv-LETiI-!i}J=lsE=we7M!jV@4fj?;H5~GeDvw>1hX&6_x~Dm zV7@Rk5#L(2@~P*aU%o0YZ$;j`A3b0aYqO`#_&}P_t1;Q{*zg^?Vq-Z0Y=*xWCU)iO zl`q)eRZQ%ycd;-cKiW`mQtZWP;GY-pv6p@k%Kc3=q2dU6u19CB5yY;&vb&q>63C>vYhc>a(P5TvW-jaV zgpk5ucz*PVVD{#l|MaGa2pu^Jc)$A{5shuxpMAeuE_{hvoXvHXp|Hc4IaxOuTG%oCE;G`en ztejD3Ndm+W8S=09GWA`zfiCc&Qz9S4F2iZmwTnYsd+*n{{5PMl5lSpQ;(#$pRUqew~=q6sd_hfGoE0}C89XfUD$SGOH*tKsb-_1|i&A~mf zxx?Q-3ZE~CHBniXz0=#=tn!<5YV!jjHh2r^i_pY z$A|FBb^8$RLPC#apQAKxiLYN=8Eikmwuva}eZO1IfAIe)=ii{#?Ey+ckKc)jwJt|e)`2FAftLZ3%C8( zzlNaly^;UP{pT2iH6Gm3>rgGtTmBR+pC3KQ)G{v@j0Q7P++j8?-9C*}F@f=HcBVqO z@?L@%>j~^_@8ny1>hR%HM-FYEv}5{HR0d*Iu%1}UH1HQIbcrvK7+`Lc(UGhF6@;|wgl?5Po;v9 z-QKj5!jeA=pl#RyPIMv%i5FKl%p&&7ze0jq^!yX|&(ft!e>`u2h+aHU7+Yc*RVQsZ zw%DL6IfXH4VpK`cXUY}w(p@2VuqX;%M~@yl@;Qos>o#tnZLL5Sud*Q~MhlEqAfV|n z2)%`Fe;sk3HkU*pLU;#FH)1z{aiI)I;!399=)aFNWHAFuKJ06#odQ@8+bOQ_`JqD>2>k1sB((st zc$L5DobNhO;SUgfL%?ex<}fHd{bE2l07;jQUesG z`Ectv)K!JbDG*7|D0tD~_lA4R*6dujQTfyCTPMwnt7m_=vk5od#kcd6Aofe#^7|*@ ze?IX1ADDfyoX(!nq!}?fN%ylhxsw|B%h2$ICMb<4+1~z7#NIlDQYrZ>{Y3iww0j%@ ziiHHT@HQG@T?WM?rQcJ>k!f#&9)Dp;$#fQUr*3d)e-lK_- zk;v(yY~Taw68ar%$eLxx2vk+7KGpGF^h7rDA>w4x?jb{bbLajIukrEPp(95=05RP1 z`yrFhA6!&E4X=VwpUtsGWHaeZJ;-+W*uirRzv_ED$yZEUAv^k!+5CW;nVmmL=oj2B2!F z5a8Gx^taTJ!p-(AM3|?o(9p($tfk<#(1H|!LQkyu_z>ZfAS&O)t&MBiuUthGZQuU= z8)Uou!lAc5my!hj>mxnSt-v|3DStyN;)5){_po*c$s4(blY>QNYYre@p=`TzGXvRc z7uKOS0EA^w4pS^`7h*O{BcsQYhL_ub}?aB?5B!`9OBM_BQKGejm&6jBveBW1(jVUK`pG z*O--E+P_r{4_DZK?k$Y$A76U3i2Z2U%yn6JX#g`(>TPKCxFO1`LSZJ&_3cF)kq5vn z`P@LP*tHAII`r6-ixLwHO9ES4Ab9}3NX)lLt%hh}%>G{Z7Y&|_|2y9|w zMF`#u4qin(4lT_l)Vfe&+1m6POUi`)@9A4S9H!z*kFv<`_Q*n7>l^Mj_Od1tKIvdo z*a$8~B8Mf73QP^D6Mh6SxP!vDWWayYUk$UR7|1Hcr$DQmA`&4JdW%9S!>y1pZi$k* zWXbbq@JpXWNls70^-ILDo(3PijM7M9U7NnGt&NRSK0bT(<+tB1ENnY{x|A)}o2{f` z)a!#@;ws`72R3p~Z!DwzPZ&FL^tFw$>a~T?x^?4z6nqK&-&qXKZa#B<7%_X zQbEkJ%~Gti_tjYt%gvDEjpckmb|16rJ|8jD=L<|l5LbJhnyO9|W>BCmYU2>svr7ya?mPoWV%Z{8e5 z#o@te{ZFglK2&k`3Iihk24b|$&5hy4VYKR?C^0bFx(=8yGynrQ@GBD4K4CRT|w(0{1ZlYbJhnVkt@2I;$6iusI0%xor^ z1Tb0_Ssw^-)sVkpVtn?1+=#KZ3z*nztEt)jifCnln;=$uXX2Z0h^{wV*P~s4cmeCr zVGtu6g!^H0UfFl{=-IRD;2G41VMq1F!^c@Y`EJA>e{v%l5MJTHsk}>9?cX%QtN zJ6_Kx(sSDQ3M~8r{WY=+B6c{8K<;)8sw5xR`!8P5W~zyB5kaH1DT@824pzq z%av^f1uXd^#e&;n{UGK=>HxA?QE#IoC7ad#SI)i%WbH*ojzPGcVaMRmaY&~>eet{0 z*ycU46=1zU?Zugl5HD8!WYL3=?&jX~wW4#petoQYGsl4p(H1a*G~NhYgxMl~iGP9^ zfU*4`QG_VgPBvCHbh3x;Z4@o0!Zy8&jFoP)p7@PRZeuedVsRk_1K&jN<$8_8U{a8ev{H(~6v*8v{Rb@gpb%txG6ab%ecTdMDqEo_pXewqM<5YJSbR8>Z(4~rj&-SgNozpwQs8Zp>d^X51B zY#wez=N!`JK}0Ad{Q(>nVYZ5fBu9}rB*5XZs9g26PTWo^3r&SCYgax2Uos1-q(VIg z#)0?)(Yj?i>qplH7BXN#8zV_pHH5&dff6ZWQg?M<AM+Ml0K`JH#4xeU2^`gAt~;h*OVEFrAO>C% z$4FiacN9d}lcY|kdnPFmh~Sz1DcM5rufK$_*QXzyI(q166D&of61W;1FnoH8n!a=g{6jjr!5svf>iN&~1YxfiQ zLrQs+>j$x5!P_)zxhP-REshCpuMiGFzaU~)5M$jz6;iq3zY3!o(wOW%AcY2%4M|4@ zF>flQ@_;8j@ZR?47knoNcJ9jOpL}5x^~P8h8|#!fyXtT6k1bk!OIwZL6|YRJ-%A`L z{uB7O&?>|;i;4smks4AUYZpf(vhgEe5yrv}&jhmJ2rw2ZVDdl++Q#wjD^O=P3rQ`+VU&AkJU(KLFA{`PqP~$T0z9Ancbok zth^%D0A%R3^w;-;BEuTyBd3lQp^y^>F}Hx4qU-qdqVK7~=Oe_H_rg+)-I!*LO27CY z7W~alId}1`36l-BIEv$&)(d0=vMpSfAVxsr6AL5sgPM}GawL)w=NO8FRUWpkN+V1b zq4R*+toX99zYM@;i3l-qTiOm4Eh`M<>jD_To+y632g*5$8a-n;W!u}8*qBIJyb5B7 zaK1vG0u?k;s%2LW)=vn;`UkykuOQY>;O`G{t|*AT31VI`vK0Qtynu!ah<)#wzi8gF zHb#HJxij)clM&DxnCM?^3iM~nV z^}0?GuY39BYr6pq*%TnFH0V+1nXsX$3{aOUWGh7GP7Xyf3()FAUvgKNmt>?Zgxg!eC8+5WH_ScC&h77AdhD_>$` zj7QcXhOsK}Kl8 zII0rIc$aCNtUJr)%Lp$gyS*>W{hv)o=<0L-b`zVwap`p{ zG$_hmGUMFK7+xCAh6wou^hNGUtPF`26d|&c zzDo*TRTTi{%Qoa9b!+wcOejfVul@V`uW&@|J0C?~a}a8gPb|_)bpui;dWjC2vg*d1 z;67sS?d(m(n^PW7L$(KInv|?h&t8F#V!`qmn^DngA_)6<@y6jG@}q@|S1Kv&>=EOV zOm+)#to=S>Tm)s|UiYmeZ_`+ckkXH?)XH!+%N71y7nPI^SVF%%VYh=P%j328Fc?7+UrWF?2iF^jM=&Q_&X zLXuSqBx+9i1*vV>mYC;OG9882Q&35Yu8=WlC3d-FZ-hgUgd}g$Xp#r3fJ{bJC;BJH zGCqH+k86{jzpvF^2SCSat`WSHOL8}-n_I+yg;6I^xr??cd*FjW)-TCL7`XaDtYQGf z5cb-)bL-}Kj21G(0KP~K_5^PE2o8H(d8IA4MYoT4U#hD9^ae!)WfF;h+|f9Eu$e)4 zyJY`mjqD_YT3IgzIVGkeyk^Z7M6=@FaV>3&2=hpgLN=fFd?guKrHK9$`VELdoIY(H z)!2S%2&^HR2~;AY#-^x>C6GvAflztqB|v&%7))hC<=lroQ8SDxaw4wMt9;~Pz9nC# zjVuWwYI9535GPl#*|Ot-nI69%-~eoVWLmK=@B%o+*1Eg7Z-oGrR@-E|2avqd6%)je zcXPWjvJ?kRsmqr|)A56&N1OUi_MMdOx387yK_7aB|3TXS@J(#ta)f5V%w|&%tKy}G zK8Hh!aS#J0Ds7rQ()v+qaV3;V>@VYp*iPTgTM>k7XjnrPql*Y^fK^$p6yzsOpmfA?XdKY#HNAU5v>o5^I?>P$BA zN-CtP_3IsB5R)4d`=V(cpCtnxP-9Mk|?e zsU2-A42+oxAkyi%b((G8LV!XL8^VQwm|s^8>($^=I6alp5VX|tjO&D2jg>Lmw#%a> zBIZo8so}*QW|$-DN)RIvdqqI&uBimE&hyi0HJJrMAOn|#p&=|b6dYQ~fxe~fU&RxZ zoV)!OZ+vkS8~gk>hxYRb+P@uSaQFtM_J`lU^Q&|3zDpza;hu%dlQ4dx6Qb1z5stD2 z#N@(^k@SOD%n>K3k(1HC04XUf+U`VFGzpBM5yRkAfgc%nA_pK zG{`z3Z%D`!i>-?69yh>b|G?K@@Ioe#z4gyLUHtpsZw9bt3YK@aywkYlohNBXKV(tB z=!Ds>;hS%-CL7Q7_|_xYMDQny@ge)M2wRD?YkUp#3Sz#XC3;}6g?q!kTen+k%ky`< zzI}Tgkd@OHBjE4o(&rcEw~@$7%SX?P&IOd}AjVDUwAlh$5gW;5j3iFku^w#9G_AH& z))4HwKmNG;+U{d|#swwC_#_GMm`s{fU`CswhY-e_2;k+%6$rTG82w`dU%UrmJahKX z*wsOr?x1z>;5)zHB3@zpp#YWLU4Wi_P5_|<0&@9s&)VzMP=LTbz&@5=)?s|?56`~ zqzVt*vVm71qaoL9fM%P^m`2iw7j#qI7$+>z472DRZ$zlbDS$}3S@-@g5`w_RPY z^Vn1Qj;=z3AeJwr6Q*yhp&AO468m94!si?@Ry$#fg4peEXj>>dgV|||MVQ6_*1h(X zZe@8F#JEV2auI4q#UukeEKGrnmK1B%fR^COs0_h$~a0ZRyqk48^iY?`*lVq^Bvnz%eQPd;IC7 zlkrk+y2Q%C#6HG1kFnWgIlql-bS#^J1_YIDoc}~LrK&M8Arx#vSAH{^tM0pa<<<9o ziH-g5PtLuu*|UjP!=s1;l_EBJOL<3f)trYZ*0zEq+&CQtvX0VZP}GNwrb(WPig{z) zmEq0qRIHgP^B{wpczAUjo!0~RL;r#Hvd!PV4dqfiQcSGQLM`bkXTFOo&gT4jJ>*1) z7rc=s8qLOlO-i1uCZG_Knf2bC^lg%qY$h{Wl8;*gSSPSyWAadT(lj!{QVI0DP|rB6 zH4v!2McAL9B?V$!P`Jb06CYj%F$7UrlloIDL#^stcWh3Cn(jmhp4u}uQRaDAhrJq& zXg}FjtXj3W6qY>_)P6r!<#zWI_eG?NX=utFtLO9!l}u{N14GcXNcZDscGMLT-n+JM zN1gt&E>~SSmtz4Qna__F60&aIie(&VV8*Kmfohl~G2|T)HFe#LFw`6T1T+o&D7CyN zu8@sIjs>4QKdMeo3+Z51%h@Ck(MrpcXHa`+tn+RJO2b|NJMrOHABj`Sn*X;stqsiX zuz($*Q(jn_Bn8wl_;^@hf7uG7hGWt$Uo>Y0Q3f`;b{wY!BuBhC2I(j+^DM8M{Te|` zxg@1PQc6t!FJ3S2db`WD7R27?mA|$f<;)hD>ylb1DLa*TLd0hbmRfTudVoee%VZ3& zz8|zzi5^EWo|lBir@ShaEmFHfPL77Q#| zzTjc?#g<_28IvuK0q3GTc@$3+?Ie><#ZVAp!HgTR7Xcy0f}$@`FJtibUb!WS`rn?W zy@NXIXW%6!*2Ntuh!O2cVQGyS08G?p5C*|O%)yLFM!am&7Yg6MNv4jL>bpD$OAfptG2bu&%M1qg-L}a@9I?) zm!!K+FP1~#mj=-A8^7RrS9m4cg@%KP)d)-mP_ibJZF-uo5%>e!;P#Y9$Ssn>c{d3! zT`9q{#sEL?|IerBkY{tqWLY^Df*>_GPXngyw<8#7tvs!HnxD}2G;_1-pd5Q7xTX}J zymDW8E-E^(hA*qhE^bpi3Y$>4s^XK%&XzPPm|L=z3Q&ni zEp%b!Um?>&iNvc$IAVZ!8AN$lV03C|TcEv#AE177<>D6~9X`xG`R;1g@cQmxVJ*-+62+0_3q@9K37ssBU4xN5 zlzY<2s~9U*0+@oB#v~be&gQFnqY6q0)lqX$omIKBkxKVjID~n5Aibp#=!rikTEA5-l2vtegHS6;gurZh1d>@Y2!$CHvR= zo1hDytiIEdEG;OBAP)y(w3k<|u+fm8SOMMXAvE`IRae{cmJt+Z5wqbaCOl=cXbQ(z zb!*;S-y6mUISRF|k$fL2r!-`nH|-Es2~UYBbi(ax*S@`j0LC&mEU_ds3s$dK9Asgc zpC;1gZME{?2VxL%25`l3R0g>uNo=4Guwb9jq&sHYkf@+;e!`ulN>y}5)SZ$>UX zpz}ipl_AMR1aYn)Q3YcEg0>6qhVK1EOEMZMC@!x}CRZuZe!)-j7FS2L)hkvm{GmZ? z-v3ovUH#0exeNbXUBrZ?FnrP;~OjrEA3X8!2Vv#Q-nYP*ouu6+~2sVn-$FBeUAXYsbyn7Ht&% zM`3Lunzv}y66QUXw^+Mm$hwCQ$+9v}DN{CK6TV8vl$T10W2E@J^C!ZxX57#q>N7O>6XcPJBy}r* z88f2UkRj`z0W3a1Etk`fqoS|k%uOfY&2|f9HGInQ1O=F~9iSw{sHIYx+7;cJJ*XG- zgBLugk4~{n{0SS(!kanipRn1Jgaz8@&&>Hx2G6|ZE0!!-v2wu=0&Kxgo>`o?eC~qB z{>w|B*J(g3D{+{y7L~UI`{7|37|Fv&VfX_mY)GTL3c{m&ZR8`mIAG&>Z5J(zK`_8j`2il;?7X9aW_@0-3m^ zWUn!KqSnX)Pl+nTB&~lE$K7Y!t$UMxKqj)do#bSQiutbPE_B_uxWJC#GXV%qJLJR3WX{v@OSB@z+)SVZ~De> zw6~m=neyU!|4p3}rkL>de}IY2o44ry*z{fSW8XM+CD+zn2s5~4O0qB_8J)1JYb2_n zVvAh**40~voA$k?G|WvurLT;`E9R@x*)^$NP(z!<^m{1 zj;?-0+MgNZ!&<5GW-Y`hS< zp`k&l(;=P0=v0wymq|pNw?78uRmGu=Wy0ex+Onl@@0lGKfxPUhm|2aL0+OBiU=qnP z4G*fOrI-|nKn$%`9+cn)o%PE5;>Em@l*#x+9%PIQlaH~H(SojgD>T^dm$x5dx_hlg zwtrb}Z6hD!>O9YDU)hWzy13`=#eo5o*%<83YT;~TO4BQHiaau>)6y7+0RrWG;s2=v zD^L1AyfbHM#idr8)0t7U*+3}nl_Omq2Vdo1tR7~!20f6nushjRvz=8KJ_NB!J-Mqq zy;2IRP9l0VVi2E1K!#6FK{Ps=#>((a>D~Ys$nixOivY$(@Azm0iX)+`MkYy0Fk72n z$$=^)0uY969iyX|B94rK4qP4>8~F0-J^Ai5HA1LD z696%aYh6Z-SrN&&C#4#<6x1SVmL@wU4+ESr9#NH`KhCc>d;y1aOQIxydu2C>@r{rp zb59C$MPs%=5KBihCc+q?f!M$ewzm$;20f_-!s~p8x!*lTP0(pqrwRU%Riqb>?yxiB zW^&3)CtUxs_kM=j5)P#stpHgBTJKi2W?8Tib)ex*w-t>Uu?5JNSb#a57ExTSRyS@U zya&zvi9w7?Nl`XUKrd5kBn^|qVj!dQ^~@;HDtHNEBMGe0RkK~}OPPvSiiIi3t4!7n z@}68enj4wQ%Kq=b*8^W%y?Rs@=+W6jHwZ?S@bA2P=%xj18Z|E{M4mzK=@A}Tuk_r6 z5jPP~p3tnGtPsMba6(jce%KXfgtS3+c(+nhZn}+HDSO~GH4?=T>JiZ<1ix|WY=At% zXtqR(fyz)=LOO&fGdBh6k_rzj!QXVB@&#S6p zH)+arIE|!b8JooBP3uD*x6=A?#F^8_bikT;R-Z(_qVsWx@^qFC^VA) zF#k~^w(!Y{Jz>9{1C_>R%V|YIQO9b1jG1rD>*z%Yz6t%A4I9~q>uaTpp0HNte~cS- z=Ucaxa7m(3uMX0J)@n^oUMw+_Rlu{}W}(3^dxFfVj5Z-bwE~u`R{Gd}9uUMt+AR2L zK9zhvCf1EtLV&&2&7DF<%QVU$O6&%g{6@}u$gbZT`}&Kkf82FcRH4Kv8&@NE&s$EH zSlIE?3eRB8x0AKC=0_XI7ASF9N@R{k@08~qorE$W!^y@7KoP|T z#=-4gH$0VN`eWCKVaKj1Br+~bKD#U1tkTHVmlF)l9i!9tu&}Q$9eC}vBgkhRQQieD z+j{T~qFDbRvf!;j8SEP;j1xS`v*Hm3GDhP|RTXzL^0;K$gq)TJ#Eei@Gqlv@fh5`Z z>0P%Iw>fioV*te#Gd7e7;xgQVL12$k+*t)NQZiWn9YJ1TFC~g24 zCiY8-`zKGrS?A3wwz~Ymae@lc$$Tc4Ji@amO^bk%Q6dVwvnG-Ng$@Qm;axt1oVM)hmlo?ojD;Z5Y zIYv@d%F2oc*68s|v9JO0%6pxdlY$r5B#;4|*w{5e%+)Q84E?cAq;$`p|LwhzhU;jQ z02u;zAoiP2Ru`Q-9;T-2C(W*)u}cSSLaV8E8lzF#`;RX0KaHr>%&it|89R%b`7s%# z%EWJc9_1x*2Z*)5wo8QCQJI|7sYh`;y{nTnL%*#!2}(wr=%AxmrxvEVWd}q%L5%TR zamT2cSA;+;XMzHYlBq0-Gk23)63|AE?fw|8NmSYZOo2@K5X8umY5p9O^YO>O{q4Q4 zuV4S8sBOH4UmiJfm}Cr=q%cDqiUO~W_g+j+2D=~@jONXGbcj9vV>HvIUZ)YKPRgYR zLCOpS87dkdubShQaaaWVjvnR3pQgs)W=~EN1+m!|f9M*PVpbR>$=uk*i=)wwNTKCz z>Af0?3+SXq(x?VYbBt0vsOuS1dEk)u=*Gq=!LtJc_d2hAeC^|ph0Q4|T+n;&+u1e7 z<6tIW$>({_$fZAunvwi-=;*1#|M;8zs~h98!6nHB*~KkNdQJ62H6hcfm0tCuI&)v3 zaqQ$?+<_7Qlj< zB@9F!nFP&JyfSF%VBW>!S=2P74--yOsLV_Qp^POxcWijwfupBBf9u^XTX@yPYf7@q z9OEy)gOG-KhjB#m+L0{DJt7U+9C4pE7rma#U)}if zYk=>BZgEWkP5hA~PWtgRgkfhb?EG(!ef-<=*7L__g#ZPyQ|}!i@SkHg&+ICkEy{1gOgbUNblxhlWp$$1qr1!9NRrQQOR|rF z@qEhQrJj+I+{knX(1@rhM*)l@hzVewVqm{LPYgSL0iC#`1Tw^6-+b>duQqO^k0>Oh z{!4@Ps@9uY6YXAkz=YQsXuLi$Bb!4QJ29EW#1=Cm;Vhkskyu0E4x{nI`D(dg~CvXZt1eqdssLZVG&6dFH%-uph*3 zf)kJYF3E#{V_O1Zd525={_Wcl*q5_DG$5v5^olJtqaJJfs#tMzNHo;`K= za}cBV?`Uq&fEe8g5=?-TcK)b3$m3Dygp>$^Bo%^WnrH0godm{4d$=4YK`S#ZS(rRH zbK~mO*Uk=8hQ*qF{+kna#&+uIbjkGB(}b)}hP<;_Z4mO>ktR+vt2tu?X^RP zKLD{tAv!P|^bYm=Zfc#}*qXe#%kq{40c=GcZN*0s#U6ip5_i$vd1m>7MT-qEO$yA( zAWq4G>bNulAJAjE@ym}TTvp!Y4Z*$BC5^^J z(<9R(*Kx={?mBct0Nbz&ZK|E`p4<7kJhB37rm+{u*t%5Fb=OdcPvp&8IB(?&09%o# z%qbtilb=scB01+2)rUEYyO_*El7#%^aMVnanL6-k6ad*Z;@d**-;irdfP7iGyr_ zm}oN5#Axk@UV#Y=I}2>n10jtK+g%h{0&NS`bjwsT6`FDi$P$n)5J7BSYk*ON#z|ue zl$bK>6qTJBja!bjJ6km+O!H2FM)4on-+6GYN@{jL1)W=s< zLM}*DuU=rI)X>L+It5l9FLjlyB~GfOQlJ`Q&}H8JANF5B*m#h_~O=Buy1@kILRd)u8sBecB&zyG0hUEqYHCrJbTP!N2h9k7XTY zIF!hA7oS2Ih;PDSphQE(5?Uo0?2JB<%K1z++O8l;9SnpzTj2~?D~K=>*-)jkq`K6@ zhz}|O%&6{?`tv0q#tHE=bgncMsB$;D5sL2Xz0eg5RZHGQdUCE}(;~`05g8Z7Za*j$ zXD{qVn~k~I_e*`-zp@fN`ld1MC6E0~B7UbLLd=3Ci+b@;rkb?la9ylC9Dm$Wflc?d z$nqwF3nK_iv091VoOn!o1aey<-pFehN~A=!oS*^tT>ukgV%=W3*h?Z0jBRy7xBxK# zo6TpTsIH{c!`!`gtELq&hll_3{8;I^bJb(eKcw9lMw1Z4+8X*U^zx}RA`Ha3vurJk z(MJ95%9P=1@tXBQL;UsvYxk%rX#?1t!|?oM?*inT&!JS^vgeg7w0MUJ&aKk0VEw zSK(k$ADty;Fv*TqL$7P1CLk8NHm(n zV@AFRXJ9@lD8$M5RS}esU=E4!W5j1Ze3b19#2Q4>Wmj)BV2*Xth zhIr*j=KA`~F;!iJ7{BpZ1fUeHSisk$S55hyVt=S*6le~~Q6@omROjEA8kKech#^zj7^e^e=yAG z`iwVHNqR>{j*ovf+w4f#^X&mK@5{_8ck1kV%eH^t9^LY%h+%L3afA}VYtatj;z!F& zGK*DJ6JsF8fbi=OV@tm+8RIsKxD%4)DZ@||$XFvcbHFT8v^kTKXx=O`mfpALjMqTy z>J&~FLX6^yx@%R;tiqnvRk&;p|4=S^Y7l(Tjv&MgHAie~b93sM%1G`~F@{Z2V;k+n zu3rU1^>B*oe6@A;>C3jlr6R1VBYY}jV_AjTne*b`9)9JA#y9e5F&j%WLdKQro4l)| zx|JvgY1g``nDsVprTU(3Pbm+j%t&HpHh$sRr#?P;>KmL8Hnl3S3B^f9XhQHCtR4&! zO|lVSiEh&Mcnq$;ForT!d}7YKnO2sz5F0xe`|?h`C(+?TZ2dc`S~LkY9xN)dXaK#o zw!ADxyAAOdjHOs~;944~zusz3ig+<&!76a(q`r*n>xwQjS~eChiGPOnWJ(Hk7%#0y z>QrF$Digb|Xp-BW_ zim;FXn=9;LhZtV(EQpDw(u$TNziQyb)`<59$zRfhk_-Z}YIlbLSd}{fVq^8y65bOP z+SQZ~$wCce?&LxAH@TM2C zL~piM6DeHf;K}@n*YFb0BPudLOwC+URly?P4SHehdtEVZB!|*H)Y;S1vv3wtOwZ*7 zakDDVXVspbomKT;q0*w3!xa@^hT0kPWpw4;Rik_bK&-#CUbnK_2V~33g`|f_B-Q{d z?%-=Yl%B|Nldtju4WBOf9V`wfUySKA7E$}B1CMIwRy zf;#`0Sz1}}O%p9dn#Oo)Fs^94fZe{LqJ>OP(PNsrKGo6DcY!>{SG_*N6-_ilX-L!N zL0VY^`+nMKcJoqFQFfYH3dAzOPL)Fj;#R6E-nmA)Hprap(NfT+dwLDrc+0fw~Kq(nk_YM_dVc=nQnyJE+G-M@eJ7A!-ZBExFo@mn-w2 zZ$?hW#=d{>lQ_j{ZX(DggS;$#1r%SY zOgKqm+1@n$JDG^!n!Qx6n8tZ~3N5_kQy-t#9k;)855j2_mxZBb{PAABSv8LmN z5idr-v9V|4eks7>j!^bLi?i$CyVS2VtWzEm8qgn)Pf)%BSUY3`&zGeBuYe51>T_(| zHoK&D$v_U5FXwSR>_FZ`fTgCA$I7<8!$U|{>1D)s3~LGH?Y*OWMd*{Bf=>6p(N2zoSFi$uYN_0g-hFQ zVy`Az5_vurd1vIY#1Bo3ycQXP*e&Z8iwN}V>+3uvN&AxSQ-C#uHKGky_poj@l9D`P ziKZ-}#o#av`FY(FV{<*M{v+S+YxkglfJ_N`8^oDXe+J)5zLWtT3?$PW300vzJTPFI z{Qyc&?E60_g&o{R?8xrD&mhWPIuiRHfStm?lr%QTeCdQKo^45&-_t?nox&tch;`wV z)V|!pxC0)@y3u!)V^F5WoCM`CmG%YGLQUdid(4oHynjReFaZ{1t}e@&aOvE_NJ|TJ zasDf_`2}pKiCVE%h}Xm1suIX2;Xmq4(bVDUH=MFMc5S*R8ai*#G8R&CM_c>GJ1H;X(J6 zk;r<3)z&r5grJhDDP>8;IB3Lf&O|CP6+VqO!nPM=(nMO+JiTw+C&Xya09d~aH|V5> zOA=RGR@PeB?2C?!n5-_k)!e$KWEM^A`3?|k?+`?ON?GQShBhctIBpgFeZl_8{;}#D zDMiKa+qLg-!s};${G)$-AKkNm1ur26V63C>JYu`!-hX$K>2AbQ6eWpq_V4?_0VZ1$ z7<+V($5g_qhV2bkXE|hX3g?AT^4g;c4NGWH0h&FQlbNYi#l1#-Bm#9SJG( z^zP?7*`P*OxTzZN^L%r?Q~5 z7>xhn+b_l(;DJPaaUD;=wowW)&Ln~_>9!S~1NQs^mICLO^cqS`RVGWa$n`okX`MX; zu4a*CU8UIcC|44^f*Lgj@Qim?)3b1_oN!nx=w37xBR?gbLBFZRj(YQPf4f4=1EZ(g z3H#0^uM;38i*?X`WvJx#voG(%P9U9MC$)eJa-}^0T*S))TAK5L9m~6_ypsX9&O?05T}AeQG~rB&2Y4(1{H0Hng$vRJ;^^d2L`FmYesxPSj{RUb)A z)jn|Xi`Gq*-{o*D+m27Ln@0D|4|5aBYgQiOVtLEm5IN-;;&Tr1EktZTgFFXbWd25( zR`!o|!+2P>#vlH6AYn)R1AK^g;kp8cXvafr*IV*pBqpMZ7-l6Lc$=`+-gCx-|z7M!0T*3Htobs6t< zM?T4#&d`~)6le~q7TP_jMaRl4MA&T*yM5&g4+^np28JCgB0=I7U-yAnIa*iL}$1QknC+6)0|3H@0 zYNCO-|A5lw)9IYg#Vc6-V)^!cb+LEScpQqNSBDt4l~a)a=fRbSAch7=s(sxOp;=bO zvt%Sf*Z+yk-Sck?x{aVN>M&+QOmN7McAYD^Qhgr|M%C)aW)JwqcxWBIff#BPz3 zf*}+E>s*y$mYB*vnrK>CX<8{XBlmHJCpcTgGjv21VfSxe`r|E%-t7-1KT8`0S>ser z37BMw^W>6>aSoHTx>_s$HOw*rs={ZgU^GYL8o{M3|8$3n=(dJ?_rAXQ`+A?X(qTJ_ zK<@K9Ovce)%fe-K|6wxzxAI#;Jt5Y>AxioJLEz#9!(2-zG|UwlsLM!q1zMJ@?H{s3 zRuUr`#My|Ew!4OkFvG-JVOjwVW-o2^e~T$@4899Qp%8(@MzFa z0j92CptP>G(zUz-SWq7I+u-)P`8sC?ddKYz@OlTp8YWqh=nvDU4#1&sSMxM9CPE$C zW+i^tfXmNuWm!)b6wH>B`_T@ra4^N3W$a56qqVg)%SA^&r-Q$~|7IwcbdY75c%Cr^ zF*pI8l%olU_wA*T{oQS;x9GW&ea_*;Sk)NhRi_gu?XE>F)2bM2!Ogx&p7v)ri#)tH zI5!kw0_>vti1evJ9jM%7eU1hk9dN@u9=~2YkOUkkJ{8bX%tKAx0a_y?g>&;}8RQkn zCajy%!rj2L;p_1Xz|C`fc>2Je=Z4s>!~5%UUwLaEE@&}-X(R6yO{De9?Iu8UFV#CJ zal)P=xP%!jK#5Wb=ZY5{Os}%2A}o z=p<3BCAc_`dI=otF^|Ln^V|>^fJ18_wMiR5@DawNEBdNW&$zI zXGi%JU@x<=v^q!o__eLA%*AI5J}St6^yot|y>QhjT$qnA1IT3dYej0$LoVe@$tGvF zlMrj+|B%V-`*}gO=Orxm{!eZ71Mzj;8G72uEyLY7`o$Nc#D|BRZBt8VWo-~D!(h|z zu3o*yVb$r{#)bf!7Hd>K+Uklio8^p-j%YBz^~{73S=a8ch&ertPxYr=l&(P_1FA;Q8vWu* zUjtowG=eEo!u7*wjdwdMw<sGkl zndPWF&D_pBH$NZC1mv&l5PfgPSJuiE!Y7G-6x*18b$}PWvbL#adK&DP^PENw|N2z( z#KiQ(0M_yR5eg(+D?Jc`9QGtYO}~A`-WW=x+F=cijn{PgE19r4Dz++QQ0CyB$|=uJ zS(-oji^~$JFU%~+!|aE0zpX)=uc6^eQ%lkuOq#i*BugU7A2EMj0frz+i`C>H^}4W4 zqT`++!*fFA4}aipPPcoerw0ZGrU(B0qaj$2PtTwJc<@Oer|D-=+o;hHwERVD-gBawuAr~>G@r36oVhv9b`tPAX2Kb#f z%smcpO-_o%-K9fqq59&&XArx=@PCLq+pf0GD-F*~(n-qL%d7EATsuwdPX@j4Dq0!I z>aipnbcB#dFfoEEAcaA-si<(MXR;D-l0qi9V|(m4fWVWh&A)6AA!f#u^ne6`1XGG+0TCV-q+|D_B9;uZMC=qs^6+--Aj8S zMEmYO;j|Kx-LJ6u3gPeiNNyUOj@A!;!mI1vkas1cm~}MohW8;v|9SLM5$ykeve{*{@t&0;_&UxojVf~cP3ca4{LOEWMt&y z$w>r~EjS^5i`mmChIx%&<*Kn)d*KZ-f!ELKSiI+kHoC$FbXR@bcwaYXZ=$zpz;B~3 z*3o*5V4wdqJwClrhto^}rOup|@x>MN^fc}L2X^M>D<+Vy7xw76aBb|aboPJ)I$jTG zN=s9%Dfa2FkPD3vD3LffHkO!VjmY;AdGZ(=5s!s&AeN|waOb9mhw0Plx3qGqbesKi z4fYy{BEESntEZ2_VooPqvx>R$rVFP}+Z&qIsvwPu2*#o*ib2-hx`IW&)u4kuqE9y$ zXShJZ|NjjT_J#`f;@&;wKEG}Ltog}*!gA#c*I1RLWv)@?gJK_kG&(tbgEc*wNQ{q< zC8kmA20HPx=^G<_Ca0$p2?QNOIC&)S+{4Gd?xhB?FUqEjm|_1b%7Bk%@P-AmljBhP zrB{z1X-spOcBToQSF!Uzed`BAViOXH)djz~_)yhbZKXQ3`$@OTvX<61xwdjUxydF* zCE32MZ6$?1KQ1;!wKbhwuX0e~B*=+a%6$Gz?eFLaaK9ba%sTeI6doP?}KDABX zY&K^vLb_^iKi=C<9!r9N_R{QY4o8pW7@6CN zQ4(^;9p5~BpykFXu}W!k46;RvOc1KZ|3ABViV;-2xK#+(`HG*{JGHP5cTW!pB|sTM z$-Z{&)-5jL82yzKYxK=-3qx04C+MvQvKqf&(ZnS9O>sN(2+6R81S2 z0f_Uc2D8V%bmaK)Q$IY}e+&zo_|-5o5Q3x|MX4w7qPZ*OVeKd}N?&wX$>z}q;DLI% z39|$d?3J6p`;>ZukEe5rp%or$YDU|M{deyI6bQf-2&{nhS_#0xjFEg}XZB-fhcA(& zhs5`~(9vS?CvZUBK}I;hqqbbc#BOTk z1Z#dQfnq~(?zv+zHeX$IB^pGkZwW~Qk(Cvv1LWRasBv{~^wwdd|8dFE9;M24MXCjA7#0J;YK-?lAAsNa@_OL zP9WX=Yl`jKwHcDZw#n;ob=7oXWI(;&%r`dJJYKy%!i-oVe~_CON$4=ZP!tGDmaHwqb)xuV)5y{>ZGD}#I zCGOL1AMh>cKw`JYPM)-Mr<_#AWXT2@Pz=lxa2GIDk29IaZ*^$Hw6*%zUE8;Fsk7l* zGS60sVi*{-(s!$CD38(?tAi?qI9f9QAtrX~=6e$;_R-kvw6-e~<3o6Szz{6-f=(zz zoSrB!5b4vZ2ZlrEAAKf3fc~><{P`LOr zqsi~J8oIgr6~Ve6ElZL4zhC}5rx;5dSsc_(tg}7{7p13frAKN3pz`w+#Vi)As`VsB zChtjT908Hcjg3_dbl7EzmMF!iOY{Ex@lg!;8CCCTnvGNt8^Sunx>Oaxq|yBCrx1f4 zQ-a%a5caQ$lY_P^cWv9Yo12Oav@pPo5IRabu&{8oz;ID4Y+Up=DqVps|JM9B%`_t3o{s)99~h8xl#r&!Nk8V1MbOo86DNZK=#QL5JzlO3Z_uD|>s3wP|%qotPHQa(q#FS(ns{~klH8#iXB zPKgWa4LX8>03Hl38`v3=sS%P9LTC;;>f2$tXf{U7A&P;Aa;8>W%9!J?xUjA8+Cn*| zbShvFN?J0~J(s4?5hNASmuyXOVFrFkRWP6gzHi7_TPyr>Q4AZ~vHhjpFVo4lK}#m~ z>urnv=47Z#2JinYEK~ZxlrB@Hqt~b2Udr95eF=>ky(En@nL8&<4V~(54My=`0M#P` z8LPZcqA+s*p8Ka96OWMjA~d@c1t)}Htbm)3)7kCqfEed8;`7h5!txDrrAsh!;Gs*k zJ`g>Po^&jF(|I@TLCt%fBGmS3*Dh{IcZ2M#S;Y&JIz8QO}gJs3=yY0l zEqW?av8W$HuGLS97Y53GFaSzQl-e^cu$e9+ioJ#F7bE*V$MTJ^uxGY!vFbe@8J1#0 zF)_2SQ2q`*UX{@wOUv05brhOnl2k(V~2Tg~|CCHx{BwC%OFe z$gvxB>a-Dl!?^Mj?$cf?}GH;o1}w1DnxYV6&l^70!-oRa4m6ssJN8T(vA0c2?`E z4a-+rZs%R8jZxVqMz#}d<@(3^Rv2O2XfY2knJoN*K%9l+hsW08Z%JzPhAb45*y2NW zB`kqy^ddVLz{%e?sDpz#tH+#1HC3(hpqu%uT1BL>5+uZM;KhVthnex|gU#WrB8VR> z@|M~dGNnNuP%Iqg;j-t$egGf=H3)bFln6)&R^h6y%1g5X5UZeB9HOuUfecwIF{c+af~xoERR3d*Mob_d4B}M z{*a@%IGIUaIfP_H6_8n!F!I5?$HSGjG-p6<0Q$$c2*c8XykAP%Yx9lB6qcZuD2S*r zAHbZpE%)yb#W1pedVYOPY`gy~mmq1q$LjHV&2y!vYl$i)#V1|kR}`%64?q8_tUvtn z{r7(%b&6x7vL}0^fj-WCS#{8}zBBz zzd?ulJMVDkHaR&v6XRqQz`q!4b{zgXu3f$kJ*Z7LV&FTX)5*=)R5a+?UOse zZ`|Vp`Z-;YOtvJigx(63m6=2`A+t!@tI)q-vn31Y3n1mHtt_y;>{8N5x)Y8<=sLP- zFzsr)DI|*R-@hNpP;7lY878*X?qB4p)g#pm)VbN_d;@lS8`&2ZM`9a-5z+piVkeGa zW~1D+jn8yqHf{q8M4*`k{ae_vX>+l8K8>}drN2!)8Puv)OEkziT+T_DTLf+=3}XIw z3!)gBg+k~>h=F1RTII^=!jc>tXU?mIuaeqVr8#BkN;xRL+ z+=ROn=Wi66!I^Ra!|&U4#k$1TKH|jkmO6s1Ih_znNPAKuQ@4-$+moH zePc2jlKz*rvww=K&iDMzc-3`RnNml`hZ#%vhf6;`oNmt8bed!5&;+`n)29a#(oKgn zMAJz&&`AJWV@MDu*}L|Ra!qY$R@O4N@Xk1vDR#BWS|^#iR+-tFxCuGkO=m&~3Gk2X z>vM3%y8A2SDP^70WXGpWofV z*ze@YZ!Z4Nf4O)8H5)#4`(9jj9+I%^qLS4Rj5XnqH&+$1H>c=%+|}O4u^DP+?afc@db zzPIK4*X4^+Mx;;69RDJ_sY&)8oJ`w<7$OP69PU7NfTDsqV^O!`*4l9A>Hhv>+jdhL zd#x2m8Ku18Oq6QXdaYFV*)=~0{hMN*d>43u1AY@WgMxi29_{uh}(W5LZl_Fzx_;2~0rafcIndh2Tg+821>OfWrVaf*4 zN?H9r*VuOCGy{p?C9hhm+Ur`{Fx0FHy&%0yL5%ofKE)mT0nJaqFR7~Bh449q-NiMk zka|NFG$C9k$Rw!cb)}hd+M^9fnI&ZRwd(W8^J8OJcz*^7LoH!ttyTCzhmoLA5GuFO z^7TT2tE>{jesX}v1n)opo+)wS(%ZzXahK0WDwDKBGY0*FWQ&T*B6U6#vf|U|FS%L*N6si`4kZ?a71`;-d0U1#2BQAT5axGe>D-!E*wtaz( z|8caas%j%qn%F6?OP(H6xSEz*v26d-7Gy6xMM!qPl#-|!#AGfBy+qaowv3`sIo5c7 zt}w7MGw``UcH@n=4;)|&;y?fXA|pCCkKK0B0*?DoOY=HMkF-JtdU_!98$t+VCU7QB zNFE(Ux&k;S0D${Vt>4LA-UhHU{cMm&SHcw8t| zlkADP`kG_B)T^(el_F&^o|_xmb}!P1l>N<=p_cb47y~hQKA^V%F;FX&OC_^zrj2qL z2&+|AONoK5gO?!osoVni%|ElLy$eE=W4D>GS&_KaUhvE5+=AFWz_%OFb*4qw%sPlc zJ1k_d(SAA0bDDC-+9?JF@HKXHmmscLpO{Fcrof91)={ZsVyOuFThPidh*d0RP@!}M zvjPx}^Myl#E%CnmFE?rZI0%HSp}|57$l!XrPc3kwt(_hZw^0lu4e6m#&R~!^LS_J< zX3C{HI8j(+6T~j<+t^R~y&%>DAw(1_L);q9H5&yv+MF)nb@8vNUd$?n0pu0XO4NgW z#AFnHP9LFL!dP9fS|S8=TsK2R{q}-hOr-=Zp73ExLq5Pu`!l2tENtRm;uuU|^MwU5 zAp{!-GU-1daN|%7ncj6MUmcc|JuQ0q6lJpeW5zzGgQG-D6QbspqN?f!R;d1 z;MOuhhm?S3*q;HjL3&IJ3+O#@MTJFt_!{a<8a&%T1zRBx0zRZZ1^uNuwWQU9UZJE~ zcQ{tV2$=6)QUx$+4$>8-PCO>;$@}pVtftR6>es|y3S{(^2x9LKVst+yCOmhp3n7%z ztdz^uda0Vrnbxg-VZU<$+8t}V^wO7a6TY(B{$JpyJF^{5za5LXeMd4^C`_fg(C)mb zqelr8V;;a)G9esi$Qgo$cY!831}qA#P;{nJEhW}{iE!PlJG>!#W&&RFAqZ;ctWOM7 zEFz7MmYgYvAsjP{E6(6~?|DpB@=nS|y>pKD>d+Vi57b~|0|RNJ;G>D+c(m^aQ^w-b z{Xq;W2P4CNcn#K%Tp z!9*Xwe{+JEt`_9ZD#-*RZ0j>|FHC!3G}L$37DH{1UcGvB46JNkUjPA{aH$SGSCvpw94FGv}Kpa42He7Sofu-P%*~p2-4oj+_=SdAosm1t%SS9FNba9ZAN%W3rN8hzds8U0? z;9~a%LrXa$MjaK|63j%%h+|=SxX^9m#=M7n(RUcho{GZ{s1; zp0p`01jM=<JwV>2lq@4q_PkoD{FI)? zb!=0vFg1oLw-6H-C?&5L$+i4*yi&3b-;h+_M>-OMqfAOEQsxoFsH;<8v8goc$^o57ES1NSMe@3?8<`9_lu{Rx`S~GCIby@Z{^Kq62u?)i&?U-(lmx`$iJ;tQGz2m9mM@cDL_QYL zvnmW7b|1nRq)OS|Mh{sa`w7c&n;ms}0OCu?WdC(-EGIPrIZAE!d(cF-SC-*X5X6X; zxcP*u6u>_F>dQ~x##4$J`-hLMbE7XG$BqT7=wvgY1%CeEK{rMCY$_W`r3j1jI9`=; zS}p8Iu5^PHx9rvy7D^@Zi% z!I+fjV<1L^BsH~zc@-HevnGkC#M?tSMt7!m3DM5_~3}T_0 znh4_upa;WKB3>Nk@*-nCPG$RvEF?9sbhv9wzCBDGq+9G^;g$+%RZ5t{oeifOHjfyC zC=udtMn<1yGvx;v(>Tqk*+W>dvXMRysTQZf2$Co-;xIA&%jYj$`s~i7Kkj4UjSC&J z^gPRc{78?`+T1)9_A41f^|*OxCiNF0gN=czgKn%@*%&bC#08xz-FPxyqEy7TaY(RQ zamh_Pr)FFgftj;}hEIZ1W{Nz`fzc&%nS3JdmC}WSMuPV)1vA@U4Ia2LOOws!A?tyZG>CX>410YtbX~I0F@tEvMhI3f^V1-UxEfM6G9E##z z(T%S@k+KD`h3uEg-AqoV!Ct?uL(3^?!^6e-EL?yT9%Q5y0h2OJEI&1&4EPgN0vBDY zGp-I+0@rKhaGU&)tn8)Cim{*(G1_V?Nr7O>4TKz(MP}nDF7R? zDt)o3Dcz50k6TG$+$9^Q)v|8r#iCKFnS>aLTqu+&j-kfH`9t#p0)=SGH%x^oN()H` z@nF#FBvE2n#B2BFlo($DS|BEGrwzeYRi!vFRV0ZB;wWTKXE@rnX~cLBJm(SeCq4k_ zF;aVLqUX6H&dku2G>}0=iBc~6lGIK`!cAu|xtt`jIPuA+`{ec)G6n+=h*^*^;I!=& z)xc(qj_`l){OicEcst3G$MTFG3spUa2o^bfWxZG|;y;TuNzyB%V`e>*pOO|gqogt* z16)>v!3{%9vx&Sz<`o7pY&tULT}r3MElIQ>LzO{{*iW6Bno!MZ2`V+$q4stV+tJy& zS;XKXf5M{{F(PtLR8t=N8AGEK`JdB`nn4Vp2=5y-kqIY=-v@nCc9#3S?5b*TTVElKg4$#FwwXjZguYM6Z*Gmu!pAo?&jR!_*RH zn$UugRw*HG=;cBAB_$i-Xii#IOvZnc8Zqs>4w-fiFGC%!ic2;%_uNz#$hzJKVl^E~CMbVAobGXb$UMxyez|6 zEa$+PWHl8%<<9Hwrb3$3$2AxnL=w>Lh*$mwL;e4(cReF+X`ZvV&1xZdG_74g$kZ> zYC`0!Xq2@fJFGV3L6UqA(G>%&}h- z^yK8ondAKl|3Fhg+Sov5QFU7T6Y^{UX$LK@kmg9uIlk(ul5Ua+s7hLwzbY3@h;y?y z&F9gq*g=F758%m4=g-5CRG^Rx(FLey5;XR_4jfTE>@emC_+{KeL=2mjN<|`hMNtcO zoC^d24r0HV?zY|?t*t+KWOFjc!sF%Xv$twSaSF$Rlo?pG$6ox|^Ew5FOpc;^O;T~w z>CA8n3!O~*INwIhpZn~@Td)>eh<*GI5DCa7^S!_~$t-Yx5OYITN%TrjQ}|%VkuO?k z690P?(`OAWIxC??*RpBos83c^4Z|=|IS@;@7}psPXCclK45ngkEV(Vf6SkL7h(@s~ zeiCBJghm%sk+J#lF>sRz1H?$n@x!c1*o9=d}AV_%1sf^vrYS?$C1sual5RzQ=+HJA*JkOHMvJ zay;r~>q_&$Do%M!){ErRsyf?jO$Ei2@vrz|yjJCpQm6oVbX`>p3j8=xX$#X3_703u z9s@MWg!6=ESP9kUv61;G={l*0C164q8Gx=B+Y1kGj^6Eszh{F;giJ@o2rzJQWrMP-F2Sp`faG0wV!M)yK9E~+`E#^HmhUVt z_cEUXVnV$plsnwYe`PO$25PzewrK))n&85X84|=^m9-go#zCjrmj5lpG6LJh-r1DTEk~EqP6OvoDxJsHHG?_Sbcu7 zAWP-*#XOp>8zDR^F4dL17mdOz(A)6TgPY64fAP%UfBo(DZ8N3v{8iCQ)u4uu#MB9gMBj1%#v8(x&yP*bPh@4U zUXd57O3wkTd{W{}RF~|UVId~r#Z@31qrAj!6Rs6;_BzSBNHFek{3m+zlZMCtZj%A* zFMi&0{OsA8(iHzF-jqSjrL3cwC?$|#_?^iL)v#m|;!X^IJnSRlzkXu*6YTuS*QLM@ zVm%Nb!j{KLLw0iibZjQ$zmBgW@P5C`>gh8zfsDP+5WxPA267>eo+4(eQmjc&OfZV# z2@Hq{TUttL74M4QNnO!CFBQV0CbQPj&5Wel-2 z#0f7dQgfxeLtK_zPp6D~m;U2b9qzxU=f$T71#PEU+)+IZ9Yzl_D&48z)iJ z6>1lH8RHZR6TezDiC~b%v!-Cb=HzRw)N+0&T8vT#GSa4Fg3SvlA8>KgrcF+HZcfM^ zeEg+Bw)hey6cNBmB@G*-BV=O9aF_C!?ZF@uTrj}Dx_s|Ebl+cn^5ucOaQn^9e#~;{ z1y=vy&Ejk(gv+zjr)TgIZv1-+F|j0!pF}a4mJV2)jB;U}vCJjEZp&xEk!>O&C|Q-f zN%m$|*F)+YHf*CBaa5u$HfK~YGZ@&Zno_5efn=!)Q@gb6m}E#+97`2eq6;XCObx^k zSpLjt6u5fZ8Y%N{nnv)+;o-Gwm6~Ccv0=4JP8vNE^T^l+Vw%Rtg%c6;38O5Ctx_4g z^wqw-h}i7MA6{mi_Uma`qJ;H-ACSHN;mDbp%)o#)`Fo%nKrc^z*fV1^7g)sgc=GT6&F)J_ zDdJ`xpKcJ9*#68WAb_G0r7kcSYou1)d{~JE&az-BwKPCjhKkHxaNz&?OZV z&Cn4ph$pq0DOoq}@-FcNk2|HpW_@licunqw5%Kaj?_Iuf1;`*!6vBbMuTM_)%#e{9 zX@)9g!$h)gv1LFAVhGt15vbVxrDi3T2Z%_Z*`HooUWrS!3}zrF%q@0hZhT1U)^k@$ z8AW);QeCl>O+_h5*5y&B12LzUTDRZtnHu}vjA8%z=BPFzWj}L7baB>ME$nFH5;8=% z&uqyg>;q2%SWR=te#pv>dT@0mGeJ^>o>17M80F*pj|uZH`|jT5LkMuGyiU$eisk2^ z(brdwo#AcUvuFD|zy39+TaP} z+Om{OxG2lI2t97NRWj}b<28o`N9o+=^5xw16EwWXX;IfSJL zvYg~G%yakp`R+D2aA7GuJjo{bEEk}@WqV$3$ZXqlyoZX}>s-a+GDXOw1}l+;1sNh1 zv*{_Js z8heQ#R0NqK(O!7>K>eVS;^o ze{=G}u?3ALl=MaNU?ysb=d!s`DFqwPA4NW&%&6i`b>TRMG26_sH!~xYmc9^9G=Pc; zN`jc=*H#hMofl^d=ZTU5C^(6wURMb_;P~-mDOZkR_`Bxkce?3^7iduX;!+tubf~W* z8g1Rt3t~TiWb0PJvH3w3h87^=Y+uP`KrELG6RT3tzp>2i+vL)K!t7S){Gr|4sF>yf z*#(iY?=BoW{;tK9#o~0ynSq)<=-`i7Jdgo_zdK}^Xog{S3~a~WMCbHWQ1 z=b5X6nAdR+$hhsl>I=L&!~-@2%mPo4ck8-vyK6~PbILUzr+7@Xvkqi}Sl7{^dD`Mw zfIwPfvBVbyvGw-uL)~m{0+=Y})*a<}F2NcH@6hd2?9t{jrc~2iE^+mDa*N9Y0|PAz z<8Jo$b7$TKt}764%Eh8dwDN-Jq`ZDs=3jef?;9s)&sK;{Oel==p!NMorY)Z*SS8CD zkhw>@-ykX{|BLCkiBPT{1hB9*Lnl-l9#BHGzPIIFF#_RO9>_farrE<80(sdoz*W^i z%$XzVpC>V3PcW8>5j+RX5Q(}dImK!$7!0Lttx>|)t=rXolJ&je{$rgm6129S3TAQ= zw5~3~CC{w<@^y=fmtJAVk$1sL5bNm~0kM-z3-jwUgNPV6lo9@4{Uary>05=Il+_{@ zVzTKiBXCn$n_`gXgBWlUSB%Y%W%Ek&K-i?s%>n?GUWg{h(N{gxpMh-1$`$8Ru;Ak+ zB|0JTq~=Xp2~G(tiKFIXGAd^;OwG?v5!yvakaRav3t&!a8?JlYU1@FGv7@(faF9yR zRz2F5kDle&3AZTPF&Rl_`^ujDT(#3E8 zgsWsGzjJVn(T2g+bKKRLJ?#vmD;a7Rk-wf4AoQ{VP>;?mtY7nFfp9DZ8-Acg6Nzxf zVZo725Fwk*&JE3tBW4?uc2m+DLL9S!G&xoVkwkvH-i4XrnZ!Z|+-aeu8pSkropH}9 z)3S`HgmT{aii@gPv^xslW-o(%KP4F3O19$R4rs!Lqp&#qWpTX{q@?6b-np^JfN-IS zf!)!&OQG&_(?ak%-E-#5%iBk$r$7AeLnsuXtwzFxseuX&Jc@76q{QX zO06<~%9fg+(6dSldk{4vrh;HfMTOF8S%WZQ1*%7D)o9V>v&V3JSbhYJ=>bW4kVs6S zl1>Mj1IsX_g&Z?|hdTOX0O1<#5l7u9=2cckVhUfNXnYT9PNcTIy<0x8`^3-@KHmM7mk`JSwwo@MK! zagaMca+!bkcA>65d&-s2G(D2a(-+w&+z^VZ6s^@j8m&w>kslF`O?ebLFs7Fs5a5!K z{{{;U_O7GjV({^l8zW&85eBKaZ7Q}QK%D4c)2UC^zOHpZ2+c&}Y8f+`E!whKyo-n8 zcgP`*lF6~oL%q?)PkLKFX!!9~PuV>!rVR4is5K{Er~45k2)I5F_-IsKU*G9>dyp=9 zP?S!O0L**u5smdgWC3kYKP65^4UZgq4a&cLTs+|N}`UnUZw?6-Jv-jrh52(Iw2_q`+;fEjO$A_LoNCYwL_aG;H zG5yz3$$|Y%-y_y8RNfnaZ5O&_LJKNAcuKjb+|Ou!wBK4YI`@D z2}3n09tNoABL$QjQxj7u)q|#)3|m-SvveItbLt^nBJUwAO=|MVAIfftnbKmPIKPdxD~ z@2QvM&@ZHsqFkqFY%wgrC@YJ{7GmA)yQf9O1hXD!>PCpi*sIyt35S$nnBz}p`Mogb za!%|YcXQY^QO)Ri#w|C-;P^;^SVS)q$wpAuJVi`u{A$5()#9apNkfi3O#47YDq`Se z6>orSjPh_QOA%HO(=Eg*2{@rym1|#+!`B^-G)?fLLfKVGhpI9$=8gBQ66;4#S?8GE ze2L=grXLdoPYdJfwf;`d>u^XKJ9cYOl(O}dBTY(KsrlJEc8^lk@^I(Xb zubg5iHchM>oWZljna-ElF}pv)bjn6{^u@TiOey(DI7h~yW_5+mh(!F*rrxStW8I|t zt3F3rwL!3~sA+c0y~M+qH%Y%CJ%Nnk1pFggvl93ih3br>UvB5z{N!dUj2>;sSmUAF zMV-KWCzDL`V;)<}s2362&N0%%=p$4!BfkW&*$*#Y{ue4eT(&sfPaStyuCmE?Q76|R z&koLnAQ#Y%2Q?rqT%lm?# z(at#0&QKA2)6pcV4~P?xnDCyiZZ zR3OG6B`Cy(zW4&bkXjZQg!7{2Xh#l&vOJ>Bf z8KJ9KFxcl{)e$11fBNl>XSmnDW%=Zf9(eZ2hT}bZcDy!-kVLVPH{ZNDoI|`KlmX?R zjEME^9@%wx`tYt@BWJF>eE5~avy=a4QY7r6C?$|ZxgSFzT?mbc>7w#+J1&I<8f4Uc zl)Ox|(&QiGbxr80ybfYh5|oNo8U{_uIw({UQja#8)08s;l(?bGN(9bhT=Egs@9GlZ`iFSVAGW@%X=<*-{opxC$S5{3q|ijojMX z*@+d~vu#h?wPM%z`7;M|0;8emIT&kL2RTf1!D#R-j{^VYum6mvUt>S= z)_9~9=x@)&>8daj7j|kOHet=Df|!h-Oi5PFWQxYxduUxo*&HwH@5wVL` z&WenYzegp4T^mLr+pxOB-0@75DwrBmrZ>`AUB(Tic7u%3uvXP_nVx9GmVzkPMt5m6 zQ>8k{uuip%rAjMAA1r(Xnnp(z*2IbEBx#yYZDds?t(rCvMmdf_Dh{OvdlYvm`a9rc zl?j&K-rk!#cC58d@?u?4q1p37I9Cp*1s5E(6uL zXcxLg^BsSeR*MAwv0^^JXn@s%at5muN+G1zRq4DqrUmto;QVhh**E?AG=8pKQ1-J=-{^DYY7-D zqhj#8s0~tv$W!tuW_8^t>m-YLHm*d^>hjRdvdrQtdYPvyEu$iSqfPV+ znA__T#PUJ{(L&#Y70dre+xb67Szb}RwQB9y(P?WfovG~nxc9NJZX|k}J zP16LoRr94=maWEh>{4oKY_O85q+kp%G{jvjE6_>^0U{K|02(V0#&NRw9!UtBWcMHG z=e+nQun%iO{2?=(xzByhbDw+9A?Y&!<`?k_Wk~vVS=p95jh67^J72Gi_v~el=Bg1@u*wP9~P&XvakZqiP>Miy(dS@U8EFng#0AEBS zP)&4|EvDR*2swucFk36j!!_PC3sx)Gk0gGM#gHHJOWJ32&}lFB`)i(lVG=4NNfJy|wwd{nGloJOp5}4}AHRXBj!-CiyR|FFuNaTH+Wn=oOTpt_E!9;3HOh18EdJBxvFbbfNq0_(xubX z<YyfAn8{2DKAC?&Q?OPGxbEsCoks+sIqJlIe_Jz4B!KFsv6&?V(#v%xWR zljNpkO{j3I=1-T2Kty`$RuG$&3W+|b3d3X*9kddAsXw}q5q682DaeqXM~AF3GBXu= z)gPidwgETp;CzqFxQH`h3YwTGZ^OZ%)>UpD5|~%KFqo z>@|^Ax-9lt4l9Ziu($0P231FfFfDWx;3!dgBxQ&v{K75jIS13=ur$WjP&xfN-8byzm+9FsA> z^4LUukKQ^1p}IObpKad!#L1Hr}0UJDZ~nr=W?XA16aau26p-=Tk~r`DR{ zLqz#NN8>CVYczg2%2aRAG&<5WGb|F4^v)^}Q!JpAy__qFv?I9hW)*=ue*v;c!Q#>~ zs_7xfD}c2G>!%Ad`blpy%4Ek+vLgWuxcKz-oml^1-Vl30cCw6rEp zh|Y%saSF)WFdelJ!|_UR0oc2vKhLK@FQX(64WNm82qnoAGG+Vt6s9zK1-XRb5pa?5 zv(v$P`nbT1iUS){Thl@e4$e;#8M@Eyjh@D?HbJ(<(#9icbIG9ryAg_tnH;OIu%Wo!1!7>u0ids7A4#Vrn~Ud9z~3*sS^J!UV6(q; zf}?2L=A8m;_$$Cog$5=IRq>Q&+91Rc)1qkdag%pHU@ca;%zuD@_H?Ow-9zKDCub zU|N?nFsX42$OZ$+^$+Tz`3KmZqYQmn#=X)oRaD1S;byDmm%{T z^YfH4YHt1&OEtWR1Q>{YAnCoa>)&}Ge~8_45X5@-A|L$0r>9tCF~sY3zde>sX0xC* z%jENP1&!-g$6I|es4MZrf*$XPv$&t1R890EInYW~nxUg8BVs5{bH5h9@=(S^2iL1p zab~8}J~P!o*M*a<$T$pL*gR#&a9CvkGk0+k$NNYb7bX!5_9jdt(jHyDy_{o9_U8<> zh!L7j=Sp$^q_d~vKu`RcgCobuDW9Q&3}Agy*?wooLY7XbytzI+%t1MPsITw*=c6n+ zkiGNvj&9&cvU*5g1jjUUeY1%k%EuK(sbL=DKegHkKZ z0CG>`ge7!T09f;yC!H!JZBub5*AsSOP;YR=r)=9NT5;V`8`LEp#z$w6)I2bF>d@9 z4)10A-*MJDb{+23oyE8`j?j+MJ?_CBqErl(sC8EA)u04mEOcJOy1gXpdzh%BJG#17 zo_FMbsiJOoK<^`7#`ZO;ORMx$k6i_8Bs3H*&ITdInp>4uf&7oydW-F5ik>i++eV0q zTBxUCaXIIdHbyb}8$rhL=5rZ)^WyDPdT35iG9|(957lCGaIKgkJ4Q3oSyGt@Pon_((&xboD>)h8TrVaM&CiLcrmg) zWPosaDUMwju05W@d4m%2Xw*VVFeFVUcAmk2UvO|6mmo)sxAIXdM z81rx^Nt@@eayUPFJuGhv{>$QdA^Ii6$WFXDBFlj1hh8)kWUs2YUl*ob1#qeVKxC8i z#8b6HKvlVsqcI(|HjYUd_-t>6saBOZjS4EymQYL3o-{%dCdjPDVis7dnu8$X0B#Ay zl)ymjIa?8bX)SW&o{>tY!7M_*tEO3&S5}5B#30^C-?R1-VVKzN!%+0iCwFb5d%lL= z^n7f}btA*4&;6S{IeK(=4LeL2WbJT?X(TY36J08Bb(JNmuy>uRn6sBc@JoF)YCZ7X zdARI`Ahq~2Q;4c0JPr?!liEqJbpnjbcY;w4FApJGk+`0jhOS3vH&S z-Y9hv2rYhJcb3ODg(E_ekd160V`sM2tOO=U)D{IXhs{M>inchpK0Vtp{NQ?T!2Fm6ho3zSyk@w|wSJ+{6Vz@W)TpFfuErk-lINS6j%64hid z@M^QhW6hEVqjOHu(vyB)O}HEFa}{C3+u~{*^fRD9r&y&4RA~7XIcy%OL?A)YT##s_ zUdmDyEJV6OGPuDYmn~n7D$u;y0$V7i5kI`|Iy;vlsnq2&&&hEl%A&17Da};aMbae& zx3vZaI){cn836?z`r#ZtjO5Z=A8JeP#ryYTtRR6)OPFOJ$wQFcyLQiB5eqDC-`tgq z$CFC<^nWwZ#rc@T+ib#>t1|;rrL-5{MH_6mhHMt!6();;8CB~JPqmBo zbIgypDyf}^FE*sS6OCSE#tfrWh=Cb`N9t3dQsiS(#$whNe=K7im&KEvw+m9K)R`!M z&TvJ-*9O#d$t1f)&^1_=s@H&&&@OOB%BkF<`tP1lfI*9dm=^Vw?(g6IAiHnf3Heyd z5Pf%6CNxR4&Cf^(Rtj~V4h{NNcbP6vNik_=L30{n( z>CZ9Y;1yi$XAG^Y7?pL*~^xy;Rj3=?z?45f)C z*8cyVA;feFSfBkY{g_G%FUr0a*7PwD7pn+s(swpOfAHv-9$1 z#t{9*-3u_*AVJmx!S6cIYPMC`=#tw~ZS!A!>6cyONA zBGVLLQr5SzlUptHnp`}W6mron&hQKLOEUJ=Kxj@~V3>>q)Rv_n+pxNF@ObCs%E0k~ z+tJiz5JMCi=&X-|j!nw{;KyYlc0qt00Wkn$m!KGZi3u}KdP0m+F$6nIS>qu}7!U7jg^@HRSFQX7Bw2OsdePiW1<hOuc~M zl~wRVFrNJK?h}l)>(8ErB=gwzPoq9a#)nfhs+`=65+aS0XuF6@Hi%qe@jZ-|_W#z-_NlG(JmXEV2p&2SmTx7{*nIf!*u$s9}15##$W8=;(qSue!1@J{#zN)8Xg{9 z$(GCsMilx{qNV806A3G`r~L=7B9RX-nh)6#&qPL`seplnY#OH|A0Ukidf3!l5Joj@ zPN=U)-T_}H(`%x1)W_B%h`qupk=IcaJFzi7sjAHNsZA4$6WmF%>bLLSotd1Oi3pP- z;U~fcx%+r~+Fc-riD6;aZk&7b%^PwlSwZy9opYDYjGO|LPd0ChsHBi3aes7&>V z+Y=KQ7I@9h&Zd(|$IMJDmVp=&i%bsmGUR#s%9HzYKAD`HMTeaGc;OsyaUm~}7>(xG z{L7jz6>0>j2#fe2qk6r%zIT@^i(mORF25hmO%RiD%Ni}ma2yZ5OgJio#BHcJG+0t+ z1EE%Hfq2aCU);j1OvFgltr!?#X^m#B2y~55Mg*~aJ|7dQ9l%Bh$zB#s*K&7*3hER@ zLjNH;j~w<|0+z}LcGeqMT$~UKyEc7lIPVd}B7#_Cawakn8(3PpJ@Eb8_c+6+Xh+<* zfO!#D2w?BuM5rpyh!tCiGtG#G1PhdFDwbp$VV7qPuiQie~#r+1#;7JSDSg1^?DY$<`1`qDD+=rUzh#K)uAfK8eFFkI;Fe*dsNc_yL-Y z{7YS3RPnK{*9M?2`Ass_fa=d~w}Mn~?A=@hu*p-`u1yb*(vZ)mBLQ7EB9jaCSZrx& zfs?#@cldQQN-n&~xr2d8js1+N9an@jNd_{7feD{k(K9h5IlPLZfBVb3Y-7hc!~Ol< z>;pC(Rb=Z$89tYMr}i@BtPL~Ydujq_12x+C3AR*AB-=1AB&%qMc%d4?SoNVZe9%Pf zAoC7HbNgGuab}!}JBLCR>JuBZ&tMWlzpU4r)Ctuu-7{6G^S55w8??}-FLH)Qk3*3( zj3q@ejM##{WFoedOp02>nR5Up)&*eioVy8;2E;})W5JZLXSnHOK&I%1QH(krhM~Xw zh21S&?Cn23J3D;cGbb0!hRINGMaoBErwbu@fCHz~@Oma#q zp9tQ_scM5&>`SOSd=T)SH6`a*kK5T!63u`ujS$F$4Y|y659ZD>jHBRCM&FQ` z2d2eraF*5Eld-@=9fyDWc5i(l5Q)W%z~tRu-FqFpE?j(_fplozSK-HJnBmFm3rn20 zmY7yV2j|=ca_ON+hFkYX>V{zi228>1a~IEv zCOfQ;6^49ZF6R+ye2c&F2`Pl@bZtnz%@-0O^5iJ~4!-WFj#Q%vmIO(St0zD?R8E`HdkLBIisJ)bTK)69;5#L}P4vEIQ$@o;-E&1_pM4_#I^+qC`${Mb7A3f*b~R^9&Yt=|ciAHZ@^c&VJzH z&-~~VQ1MGt2ofr9tKsy_vd<%zP8+(id-Z^Q~6lN;X^@DzATC zspFA_j~m#OZPdbfNCldL!b!)#AiPnu7roI+wc;KW#6IwK(o0%CcJ>3_$&v3dyXF%* z4{7`$8_ggKytvZfV5LpGf)H0D^*{t8Q~pcFlFXzOs@BEG{H=#_xsa55S9Bh+pX?aEZ(ykCsHY(8*J~q$6;;YfX`F?18*l zEGn7$^TUTtCFGD5A;Lb5*Nb_6{DGRPgIK*_D{K-|_ye1=>c>_xl{!Swwu{eFySLiI zzO$D*mtm8eltdY<=Ym2b=n+n$yNvC^_@ZLC!N1aUsIDl1x)Q6)l@z@0-krWd*7GJw zHKn@5m??FTWr_^&=6C{7Lf`{CXpkmb2;Ku z)9VA6gEAt*-*7+@DsCjUkep3266)|cGrEz<*75zjMIYFc%sG=Wvzlt=h^(%Z{%wOA z7%y}RUlWlV!?i4sa5rodo10pf)P%y}^-5o*VGC7Cp&Ea$RqWkyDnuP1hU-6enbdOM z$Op_8os>MOGrZi~+6_mn+uCSla+kBUX}T}W>+Q%r+8b1EIB(o}p9MKVE9Q;@S{Wvy zrXed~cAG;Qb<8G28B&=!r%Yl;@%%(aSG``=d@ye? zRp?R)O*w0&+HEgOQSHF?tONE%S#4WD)faAQM;WCJh6+-XTzb@a`)q5Aq>+oai1Tl~ zZC*+C`qFTu@_gK(91gWX`Du0AN8kzA-EG#PZvL;(Lrr6VK#X6J#k822!^u2$QTDo1 z07=MD9Ee5IUa!|9fO*L2)9IN>RLFKIw%8kWCY{IAx}S<=3BM%sds?BeX%Lo67>J9B z6~PNzQxDllBsUAv#I{u1g*{iX5bX=4Yt>%i6gsL$3aI|va6!pPn}eM?fwyZ-~&onO53{>q#n2I;^aUxtGmM}r0t zC5j&|L9F(mR5%<=q?Bw-HxP52y z@+4+1e>*_>W*Dl;&(GcX6c`hs4g~;WK zrS%clm3(M*Ff5?LUE%mzOevb?=H?-EwKRnDxRy^(udUts^=T;h0QTN{H_Vo{@V@34 z8CET2cbMWF(-Pwd$+1+{RIVyN8Ui#rDh0-162$^n1@V;ic00Q2$oKd7 zS^xnm`gzDFq4T_xp>9`=&8n8xp`jlx4|YT50WmHVTss+n7fq1(D&kzQSvjsg#5GO^3uP~wHz~yg)C4&+C#1ltf+LbQcclJ0P4m}g72Z<+0 zf_U;uThI*G{_e{-LT%@+4RX7MA(g z+)qx}yN8gTwpY(vtz|oTee)0zEKFfpEz#Dexmq>rkaQe{-u-Vszbk-IuNUUmrT%2n zoAWrIpC4qGNE|EMkG?7E7zyXb+4O-DzR+j2KJDnk-sc2BN>MQso`EJ^XP>)~;Z2Ce z7Sb43(!7v-fL>Z1tA8ER$}U_=_FZ@tJy7=_eLzeBqoN_SFu$rR{Lg9P8IIReB<++; zt;UkZ9=`dO@fOCI>3uJ|iM$}>$ru|ESq*`Owk~vk%2?A7PaMvh&j_290j#-MR(-LB z=`oB6Ssw>rw`gfYz~}tPztKeqG=_8dbK7X}uRS-KZ^8>hieAo?I+9qH;aOqTvmT|2*)O@1lAd1Cs zgiK6?sLtQq`L+C$Z|sSZ-0LY2OC&VN0YqX6=~Kva6C*SPuKv*^f&}RpX-Xkf zb(hUZLD&UQsn0IEnX7>sRtB)XF7P5F4{5|7E_3?~$69OK(3%lul)Z6JSyKgK*w^Y~ zX`tN$G4SF}nt;({juyQ!mCA^%uiCr}*P=yqafwbk!Uya^t7<2H0Wtn*8+kWVD=_`$ z=jC4NiVk86(Y!+t!yU?&9hq~_2&teFIvRRq=L>ni{8zcCQ?}?*yMdr-87o1A0x?w) z#Bg9<{3ris-jU}oDMbUZ6YN%FEjoMUr3}{2>I3`~+TkvXwJHo_GICMmX#*fvD>UFP z>$5ej(Z%orf|kz^t;dB4?d!@%_dmHiZCaQ>_6E=7rz?0>V*zV!3LdtE%h(u_ywum) z(SM5LzYw^V&8v)^Fa%~TQ45o5*qEBZ%6K*IE7#VnMQsOXYel+Q!!r6~QNb7;vQzT?j+6A%BgB=Xl5-Y{o zA0#1$)lw6*sOE4xuRLwMxY8xH!)G=M)ySGX=HSfG0qt7_vL{bK?4#r~EP(egvWq|d z*-wRn8}$|`J1G%$YjXqk=D6%i>cC-&(vj?ofMO;0(#>j*?(QxdzKK?RV!pQtz!KS1 zie^AD@9F;$cu6}ES7F{!Dxc5!ScH{i>ubw zjVOAXrA=tATt_pcxGx7?acC3Mr>0N6M+W`|m-5pUhleOe?HwE2+FX0~=+QV6bUc*d z4hiYT+i_<-=iOpttIhUx7UPGoFm{Ksrwkjz!1OGJW(pTF6w1eFo*Ziav-KBUvRKk) z4H5S5v~KZTgv1nYAe-XU$H>+wV5DM^8G=4%R0=8Ejo-W{noEu0T-<{sjly!J^d-$A znnnkRjw-^WtA%jgjCw1}(5BB#V`Fb} zC-GuYZ&c6n18dr1c+oN#hndJr>61aYIA=WW?gxUZU-|) zna0_y*TTGxq!UcBRLn`AvBW$F+_L&oz9@P?e#-momJPzLN2a@04bF|H@UbGnXPp3vQId^o1yTogBf+M);$>h2<)6 z=|IdyzCZzOLo4C>wZ@w0Z?dXQ25D@o#1!xjabWj3SMJ}Not_rNxRlpUU3}AwU4N4+ z&c#41m8DJ90I`EdE|2VQ5q3I(*nx3*V%mKj9Uvy#&wvag6A{W_LI5KuLj)}!eakd2 z5R1Zw)8`UI*B1FfY9MC)>L2aU@xNrG6s$|A9UzBDwU;KcXI7$_S+HV_zx2wn?X-an z_Nt9R+^`TfMNt66M);19Ad>Y{Y-a#3s_a`RuvujP>zYktyn)Tk66JGvANeIT6J*6_ zr%!P}4d+kA0t@@iO0lRbS(!sm`AfDJmN?VloR0l6W7!k$iAz96o3Zbt}kPD13rjfor*=srtRd719r#LJW&qzL9j~ zoK)bF{PqlUurc%q^3FMhQ@`Eo8iYu8aNjXFQcwj@2mn8{k^8LYEutv6A!;FhTOY}m zkPS9URs_Dw6a=Y;Z3h*<|Acez_D3@q@bL7ooay%jFz%$6ZxCbQsrC;@MJaxSNDTg& zaMwCI$A5^-cf6+s@Ot8%<07ubrtQ*$hld`DBO_VLVq-e(6O0TK6E#CZTB@Y$tu?6K z>@2TQ_j~y(JB#?gu1FWTFjj)kmx|@mGxT4Qp5q>x4YR|LS5CoZ$miyqxtOA$!2Gx?5XAl0mq-jy4`YYHP^iAi?bZ@9%%~ z$-PO?;s?QR^IqPN*`81TV~*=c?2ar~L6aFcH`4?BNFbrXqh(s;K>MrXY`>TB0Hn3# zztl<+34mbJ!P zGq2=4vd7JH+}wJ49I)t|vdmLzJw`TV1bXYX!SNG>u17`y>;o<)&*iJIfAC|l3DBZQ z$O)=1WEaRYJb!DgWRfqI`i)UaA51b+_MkYNbw@tRAcz1do|!mREadi?q7>c<~{j)mdEKie&Xv zA}Dolc`Vn^K9a;6FJ}Mv-H(6rdkI@v@4`01Gb}?Bz>sCU^NZho`kP#qG$+I50?su~jVQ95EbO zz~59oc4VJy5B&R)@39X_7{NZOi`baP0Wp|hNKXuCLIM(j?~PVU6>WW(m>a4&f7(qH}|8)5(7jkm8o z?X$b%KKEdAV{x&txHyxXl?R_^AE6_7Y{#QcG7UOh5?ln-AR}Ga)sgM}r>2!beCiAj z3L{DrO&wK4uGzn^wYa98Ufd*GB9k0q?&`$q=O6#%{;kOc*-MB{3x8n42p(Me?K{8y z_y3$%^z0OP6D7msw${M>N%`_YAmg_z+#ZkbZ{ODvUY4YZVmS${6(i#u6~O#zCab41 zxhQFo1Y>*v*z+v^Xok%LHG-e!=D}w4)-|i;m6!fcGy89eP<+hMqQ-Xs0000k)4roPp#K7XoqSC&c)V;>w$F0}Gmd&@m z;K<75%c|DEn9#YC%eI@*ySd)Pr`W`^*ub~j!kE&&n$*9Y*1yBu#i`Z4yxqpG+r^~T z!o1zXx!c5_)4sgo$db;vkIK8Q*S?U;x5MDZnbW+e)xyEs!<5Xqqt3au*}%f(%dg(Y zx81|P;KZiaz_Qi8vfRR~)w`S4!k*c}yW7Et%ea}%xw+WDkjS)=&AztRzRc&%u-d?& z*1(6ywbJU$u+qBE=gO1Nz>LSZ&E>|+;>U~5ysX;DzS+Ua;>5n?%Y((V*6hfr+{VY> z!^G&$qT9o?*~QoH&$Qsg+3?G?)x3_yvDD_p+w#)9;LYdr%`OV_($j9Hlt>4ax!LZQm(fRhr>G8t& z`p(+qy_nd^q29{H>etiWz4-6I$nenO>b{QC!@26%+V$Aa^Vh4+x6kXuE?UJ$fM1*p3S(h+s3KVxwGHOwB5wA-N>KMyP(m#rPRHa$h4fwwYuQOrqH{! z;?Aqw!>`!Hr_#Nn(!03a$*b1GtJA;~C&|YE001a-QchC>CLx zXL*vPS}&C{J7Y{?t$iNdp>Zw~tRK{gI1jDbU<@JAfrEBBkMG|5>ev7I$A5ELZ8qyH znMq2;Ck&$>tcQI(>V0ZN`@Mm8aQBLKhfX0^zTc)0Oe-h`3S(?>{c+Bn-FhkpAV#8Y zF$ne|%v_w>I{%Pf!Q+}ZGs$K%V}S({mV&O3{+xbSaVP6`c5`xtOuX zbGmnT37Ra2a}JkMtv2Wfy^=eh-}(BFAET~bnp=pbq*9Sa#N$~+BFhuEH1+V=)2Fv@ zU%$1vx%uev&6}4NGcz-p&#r#5xq0()Jf2GNtf7E56!x?=d${?wHm=Yp6c&V)eEy)d zr>AFZHeqwan$jYc)PjG6rqgSC2 z%jHc}Hk&^p%d{-)%emA_JX(X-YgqdH&S$`N_38qiOroDB5{Y;go{0D4$&-f`W~0CsD0`|>pV9YxlVUZADBJf7wV6)_r(l<|No%xAMrW2lkt ziK0Z50xq^hQXYuK0XiO!#n<8wFFd{X?amHM>npraSMZmeZ%{is%UfGs@8F=yq4Mt) zXnN1=Qvq9@*z<~9F}1o1P^+tWO`#4C@#kTdsp<|78Iq~J{nk(ZjMz`!c>6arzg#26 zRehn5A#~C0*7RpG)2WrTMWD+0NExSF#^(drKAFWYq62T%D-Ie9m6tE`wK7@u`CMv; zQtc=TOlsHh(&rCYT#K3epDiq8VmwVklulcmyeBL3Pp&*e-QNE45ew|{&CAmlG8qv2 z_>)`PH>cx?lt`n|=-o~&EhD&XW35ecijq@7V?O)xuoJ+JCMJ}6t3{+)hQKr$9xstd zrBa~9Rx~#Gbo=qQ-!6ZJ+QJL4@a)ui?R*6|gI@ojc|2z>6g-}S%nYSntcz@~B8Iv^ ztIiA5Sae@}G1Z&xBFi9FW`6Vb8?O^P`5sNeqK;LLP@hAu7U(to_ZO!j^|&Tb4AU}_ z@A2SEb%d+v?dY9aMfJ+X`Fy34t?AH{VQ*38Dk=>{r=hrX=kxnluU?wT#MWk@ph>GG zC4zLGJbbdUvQj5@Z+r7g7TEOV>FMd{+Wq@;H*RfSzA%l>lJa;OjoYo|$RM&vB+?WV z2dM*!3LYxt=*gpi-kQW4rsr8~7O6#=0JC^B8jZ!H(&*yt?QfR*&}v)H#m}E_ff-8; z#UgwTHiLV6=JNioOxWb{Ftp$A1x8dQFMg%3A$+ZO<`<}`AfYSQsszEWy?65e68mkr zS}l7`(22^Sl?b$wyP2DpE`kpADYwK(h~>Kfn6L6qWaI3Y7v) zF#d2@y2ay!rHeI$_mV zEf%-iVwLg|(nKbnNW^217{t4=#lXeE`S9?i9q7P{tu4FV{v5c>et)g}LT9wKwiQZy z=D{r%nZ1$~v!sSrhlW-`3NnMlR#&q$Np{s3yS>J|`{wJ!-g^7Q?(Y6Rqf!=&N_C&a zU>Lr;Hh25-xJvEsZwCRQu5RFIFjTrNH4Z6kmKUDqXh8GC?u-2qbT}Hb;NMf#n=*)I1S354g4n>V z_ki#^#z`64k38|_4?c#1^PhZg_r-3_?CR@NtNXM8t#dRpcjGqe#VI&tg%BPXDY$*G zvGY_z$58vg`SSymkYE^^CeX7Ow9Fo@QnT`Ojy`xW6OHzdI`7QPEY4&yNgmHB5QHp& zdCbX;d-ooHgMZc`ytIawh{gKZL5c75Fa_TiB`{GnR=vtk%Bodvb5mnuV?*O9_DjpD zo=8|fZ%te0(`ijS0c#YYAjw1=tfQI5#p|1oclv}zx!6`Gh5=<$1cNVMz6`!R1hmr2 z5?nq!aczJ9qvny;o`|qgDX=}c_kB~Mz-a)$;5z{JvQ}29$E&2Qy!+nCx8M5Pzxgv_ zXv?3nvk>D$tu#2)5`kd!!Q8Fe>u|DE0LEA`QcZGkr(AxzvEg(_N5{axK(9DoD9|z) zpXSg^0XoC`d$n4LWc2QXHJ)?S=^UL|SeVH~K};YJ=-p_d`1Cq}v6V=R((?e+zXpkY zeCzh~m4|7Tf(VAL*9Xjgk|a6k%9b;&ojon5KN5Yf}K6O-!-evi1_=H-{%CuDy7%&u@!h4(GEKdjXY~ zE9%Za0AzLF2e6?-Vmzl-9@8?KsQwf-24b(pixcnhNit_v7mEgUQNSwY`X*ZEN+PXC ze95;$>T)~^G)V`32%8N(nm2kloNB>Suou|3Evx$UK;|61%M&=AOQUx)3k#D`soo+G zNZbM~uz}b*h;8g#NvA{@pi*gnbmp@gpWM35MlpCel$W&X18T3TRwFrW+#VESSW9E$ zsWUxe6Hij;i0U1NU z5ExoktL~1g4Df!4OMt=v0I|i4R1d{!F%ksBPHAjjnVlv|@4POtSKfc}-LiQCk!rZ;EEZ>HM<;J>-do>T(IoX| zn!_9%isfRc4E@n?s)Y(hnuQ1Q*4BIl$+t^JlEi))qeE>17>Qui35_n@`T9OW*@bwY z8^jD+iOJ~{@K!d~H+JSVNs)-vN#=egvp9GC<~~$wYUslT`L{=wYB|9Ufj); zWx+Tug%KiFJiB-A-o~=tEzxQX2J%2`YiJ*0rxROe&z>7Vh{O!Y7HG5Itn&Lw-R_H~ z$jI24P71%0R;s5{0miU0gjQRS;(D4nY{=BX+h5vCzdZ5wD~Y}CWN(~2@v~aa1!7vm z>}>!2xm%AOZ)_x%Lq7cIaDEVj#$f(UP57m>MS_%~sg>(N@L&iIpM=6lvN&9q*r>!{ z0<*P+t8s0oQ%a2QkZ_ji$E-qg20d~ps;)EJ1YU~Ox<&`c0r96q0BXk>Of z@v{>r-}pgdulMCUKT{T!F4yew(o+A-rOij!UacW-nXGjQg;1_ofyNXA8oTQm<*jo0 zf#-Sc zEgrKVD2(d85SMpr5Vf~eD%mTZBa-I$TrgIP2qQ8EZGtyU3`+#EJ}}2)nM^eH5WwIc zZVRSCQe&|w31gG|)ajwKXYrsj+dDdJjg&l?FVHA|*q~M`O|vhIbz&`_0@w$T|%x= zu22M_GO8(<7Y3;&s*iKcCjdt<=q?_8gx*_VN{*i!Udi|0%S!kW(wl5OP~yd zLcF9^R7Vz*@-Xe(dVm(F245ADy1it#BOjYpu0X&Ya$~Y0f>}`-wkOk=s?r#Uu|qO* z;n~xB>sQk3G~fZK#cDvpKnQAQd7bS*H6YC)N>n_w*=$z%)M}T~v~-O#GBPsO6B&cd z&O}-04 zTsfyyD%DcUi$1 z1wAHWkXJt&!!qdUXU{eO%wkDt66~2IX{aG?H=O>koz+)EV}mW&DR079FVKu1z;X_U z+BH0C${md~Hv?Et&l$WT)B$EbL5}YqtHyUdd1DhqVT}AMfW1)%_7{=;o69up99^54 zyM?Z-6Z4YQ8eN0TkotoPF?_KL)0%2(5_8)M1(^&TiSSF}ej$-aoNQAf_zz2lKYy?= zcj?kXG$eq=Oc1ZnSFZANIiEviP)P(9p2ebfvdK>{oU2wzoeTmPL?M^^0nEPV5BIs} zmy?>LMr6UP5GB#e7AqpFCLN0+=Ck`Z3v6R!ejY%ju{3z`q#CzbXl!ZdXvg+@9>5w| z3(n_-o)QCvsa?LDN~JCiYsWdw&CNz6m(G@+PsbucLMIEC$H#N7@qL|;YHc~yDR2De z*MR+n#{4b1$ho#K_sOHpZ+1jlFK#@1KFKiEuDW-Lja*@NYD(PH6qGj^D}^qOZof>1 zp|kPg@ThVaGIMIxCR1_t!PhgFF3rtF9Uz8=%+&5gs`5Nrj4VYWz| zjH)sWvO)FU;PWl^#D@k7hnMG(Xp>05Zd`yYpy$?J6kzcLo;W*Z>tkd6%KXJXy#-NL zDq_Kk1nA13t$ko%Xlkf2i112fjqF*{@6Y9O#i9#l?QkfyE`PSAfXNJBs#DnSQwg&Wi_cC^GeE>t2-6azkvpB!u zyaHg3klSL>#H=W+=qh`#>6EQw=#WPC7G950*z#y$em(w()>;A4x?J^(WnT@yOa(UGU{9lwCs zK!lZAyV`3LvVmkc9Am#Z9oPv(<)Ls9QeCIgNDOg*W$)Wd~T z8pC?^$AzbtudK}Xc~#zk1)~J97`^Te=c&%q9Row>&JFeU${VTd0ee9tNMG1If%K$S zt5g7H(t2s6$s=x?$AhUHHiMQ!X)tJK?;KwnX&w9Yy>}2RUIF{7$liDpGRv?xK4{DU zPcBz$H9iO4kFF0=6ot!(0-Kvc(aK(}Juk|983~oC9dJ}_J>6>sP}%SQ@ZgWgW0RR> zH%27lv+n3EU{<33%)-@=Z(Ikl<-V{_rH6qf5*qZY#O>p@Gs%4Xc>8R)L--p0{6j=RZB^KGQKlt4z zo3|dVTMfr$9ZA**KEI3T;(x>y!^?2sWjFm^aclk{pQZTlAaWw-!!Z&fL*$0SMkU4J z-~Zujme@?j8c@1aVYB(jGd5<-kF;@{cVV!QAvzbu<_eO-k(G|RFdj>OGtDpzVSnyt zkp%*JgR%&*Ib5!g-mTG~?VvHZKa7L@aA197zAqg1DPdnyOiB=QyTja5w$uN0?%dg- zc3W@I$Sn{Y4@1bpMI~st)Gn9OpmymEf}$DH^Ng{?fU;f&34q|$8D(Yi2xi9~Id5tTxPuGksI&fs26 zluFO-d)Vt#UKJY!yn|kZPS~W|t+9Yue}62qc7NeIF1PC|eSQv6C&se?qzIQ;|3OgP zaQYmw#gHN>B+%^DBN^i2u&Ky;ez90oJCtsH(M1*tZJr2#(JqI*4F+!wJ|<&ie1by)*`eGhq!bOfDB=o!7(6I2cDjPh7Y%QK6iqN6J&@N+3!q}D+VS8-A zRtW3j!7(!Q;Dv4z7B`TL1KmR|X`rr$8EUAhN)@T8R9jfK)GgbRZ2vj)dzGrGTa{&I zeEBdglT@835AXIo@AI7+9{+D)wsDJK_Wag&-ddT@=1i8widVO4$|+%ECS$E{I(n2C zJGxL=T&!xftGR&z^5-D+v^KsHVEVz8mC1(>Db3^K=T9e-wzRiujj7dc_z0EX&nO~A zy2IQdy3pS&ItAKm&=>;tc)=bIX!gbxika8)H-(DaLX=S_+u6-h%|bqX!}RFP%!F(> zzdq$K6~n2{`4uzO?G#bA^HiV1TMc)29XWFJlijXxAgZ}h6;^D#}1nubUqdMAk8pZV_kZHm}u zJy}yqqv_0LeQ=0f4ZETUAOl`aCgVN;Njj=p(8T@o^Mlkca*@#|2xdJ()SNv@r>(5< zSi=Qk>>T4D~h=h`!hJbPMj(rJX@}Y=+7{soMP;ep2)B+jQm9rs;s;oMrAXY7e zt!gHVqr26Hl+rmjU$R9c{PiFfDh-B82_~T)vm;>%!;Qm5y6CPtgrkOkg%^PRe?#^| zoV^GMcz)})H4BKX>g?f+C!^_g)!160%wnptw#sx4x4_PDurJsX?T{&pZNcEZM|TCW z#}`lcd|D}3!{{k4x4OH_ld&)B9A;6TIURbl!{qN&DgjMF!_o2skQEdnOtI)_)M-;W zp9isleeLuXs#yn!eRq`*yLh~=lpdfHmIt!!q2wvLCF4iWzQTgFS9JiuR8~!ZCJFnG zFgL(;NyZ>4z--auGq)$a@Vsz1WonPsSn5YReuURL;!Hr>jBvK9HJKpe@b3(;xz(K&52pdQrOh`FpzGsA$5pisC^3kQMH*P)t`t+%%vu*V((2mGTsN0{c%WwK+8rh?kU4#0D(W|#_|MRWO zhmS|kr70G9M+iWCzG%|7t0zH-8TpSLK;&NWfvL(loPt#3|1+zW{b|n{^nh&_Tf!XbHWf0K|mbA@W$Jx0js?S&asi7e{ zRzQZ^ReM}xV8OGdKnyA@TUH_~WAl!7DkH-igOtpCb)(dMH1)#`dc>Q#rF=*vkUc%r zZ=Ww;_(yM}+I<4VddWD2X%QObq{jB?dy*U*Ro{$AXR|%6j~R-^fx6L4?IG2Y!FH3nzEQQi?USSI2NxKM z;4I8OLPml?a}?S&EVGxnFOtkVqPCs((+SQ4x6CZ8=aerKYply9w%Uo$lX#!|U z0jn%8IfdE1+BK4j8J#H*TToBZ$B-zQiS~Vu7`ym6@_qYktjp!@iVRa4JnxS^*a!IV?zzEXN1{?*BAM}qRj>Dfj|n0v$npp7WP&wYdO;zv*x}Z%z9KB z@@CNH42_||UcT_zp6DsZf{~O|31G3VE|+Ye?EY+se^b|>FXf7hT7!lfCSyfPyXdX3 z+xIGdPJRHyd?kpKOu7(o7ErLE!z1IjZ~wPHp9ZH8yCLYm2pVkDeWfW|%Pec%*4?`g zsI1o6#p0p`VFfcF?2WR|bLyfHDZ~_?FX3%!)!4tBWPu~@z~Aj)UG(hErdO&(J7CFg z5-40C%vdz@nM~v>rv=2|;1maFh)mUj*eJdpB-DWdy+WmOd&ED{MIB?WihLIe{;7_= zoT;L*Gl&^x*?A#idEuCas-*(4VcHL6c^sM>hzVAE_E7weA3JpU#_e05M=9R>v|dNZ z*C7fJP$m=kyrsVN01E$ZmkVXTVj;xV$~Ha@h(*vP_0ecUQV%mhQB2jh28O;w#=9zx z%HM5&hIkQW&%akx>?uMll@eyiHVNV9@AV+IW`YeMFX5LAnfovy)CiyrR?;z2b2;{; ziv>eC=61VdF7^%K-_{1i@`sP?Nlvvwa^`=W^JrRa>(QkG*^8ViaM-CkQO56%wvkFq?!* zRI#3*IaxD*+)Z|L$Ud-rZ++w0p)V&N%m`rDKG^cBXL>el11@?QsId)a%(PlH3x8kG zDxGn^uFQndl4y6>%+7O+hfW4f8^yKZ2VZEgq!=``NEJ&&SDUNm3-H(#rRZ*u9kYG<@oaB65IFgW7~?$K zq~vUQXUHBH>SX)UIV8h*Sj1qfT^F}h%4qy+PX}6@>1-sQ&FAqdBJ<-|VI6t;kAK<| zI;a?}hJcItwHe!u8^FYckW&2Vk}vUeVaQN5Xi`L&*n|zlS%JlUm7Y?0`qcS*)|`Vx zQ8LLU2_d!*O{cEm@WopfqE0xab+%~vxV;Dc2gG(IeW9#LqjDW--M?EM)O9KK2DM_WMv)V@zq(F%^!W`u!@m8+)q}x~1tHm^1AK zqlE%lz993rA;sihOp%(4gvUP2S%mBqQ5FO;RQ@3QHz`@i&YTYBO@+qZe^k9|kn@hH zteGfJS(A3w$Y0G7SGPUoRhB!K5T1e^gL>F^&^IhG1~HRn-tO(S+J`15?~Y30|L%OmCUA1dP9%gg#6_`KMW3-qPf}90oB~<2 z?$~Kt)-qeazuurN`^+Jrgg@wmA%|8QMjdNXd%{CHU1#|m(~d2vXMRnxT=`+_M^vZS zri;Ddq5n1cKklBN5X3$pDW7p#zVhpMe06mwWhXN=q@__4C@^cH2Ujd*5W~5i%Oi8J z-$H>kj|pO!f$^X9gt9t2#wG?bS|*TrG9IHS81;D&( zn7CoA&WLcqASyqwNRV+(Ggwr6_wR2rSjxVLt&g>d*k_%NwRk|S#`}~B=y1!%q)^8b=I-3ps~+QK6*eI<9x6Q0G@SPzV`B}-M`Gm1Q}up3{HV^ zZO~{}nKrTG&drs@f1Jxlf+0RuN5lE?i(j8UE+#sQChVc$Gx>`F&3d+5?NT)%W~kK} zB32kU?KBK*tN}P0yF$^f6e~~n$r{@ivdLT=?AaqKD_;%$R|c1jZAU-Zuhv*BElAp+ zxl^CCMQ|W4*JHJaWhBH(v<>{FL4X1;<_lsBxMathu!jSb{kx-=z>5&0?mz3Z>7`dG zW6R5SyJDA-GXrok(H2r#<}p8P3B=asvnEqE)DG3VGTbmS{`D6aUQhK&3S=Oq9v1^F zg^XzH>QcA0Le9H~hA8&1uZ6IYGBz|M&$OS^X^YiGZPDb#Qoth7iTr>GEvO%6g2gg6 zJ)Bj-M=&sD>=y5u!x6$HRkt(Q6DrxJI-4+o#m@TY=7f{tiJ*i@gu#00lKcavY^SsR zq|E!ZoV+nH@$5sa)-B(D`D9$Gyv{yb2;&}yR zAnx299e;e``0+h|>Z8sQIboxb2xBUX8Jkq@?k*SKU20>-7$#VY3vQ#)LxsZvtr4mD zq^6)lhegW7SC~iUx3#lR)DJft8o58+AITSLVd@SWidui(W{&P`*r`V_FP%GA-weUX z=+K8~8j%QDDTmd1ALECUWpgN$&zd^FoV@qw!TD=9u1-w+->TRP>*MHKw!Y5Z65f$Q z^9$o3fI1To=!1TpR_C{f>8`x8vLYF28w9Zj_isMFcns@da!=oYH?9E@H+WfLmu~96 zTivK~$E-0oy90{Xf-L8(vHE(CMibA)E2Qcw_D34=)0Ta#UmZAIkgfG~ERv=>B2yYG zaJw_z8UyyVyidAlotYVSV0nMD>VR7VVr+8<2ZeR_=*3xT4tCH3O8QV~Wo2$@62$J_ z$9^<%ZTy$!eUWasV)){EqU^fl5WD;($uW#EqHTjy0#+{fkM`5zCMUL z#-a4HmRy?r_S<`Ra39^Eiv9fbx^cscTVMOlZ{GX&?`_?2Aj3cqKe@c1usb8dK&+gbleO}goI4L6&fK_u3o#y#QZSMBhBIIW zr6j_7n?OtjFITw;JPeOu<_5J`qxG!EWAp?JDdr4zjoON$L+$sM%UGXn2=v6va05g- zuk0iIlD^eJLUK@W~8_ z-JW>kxs5*o*QV#TzW-*>mNpnYT|oAm&Fe`^Za)|OeuQ|q5~CC06+hXj6jJ_Lkj;Zw z8JU@c-Lc6$ggalr7sG1kpFO9M`c zSf-%*=(OT3S}lw9OAvz-1rH{s6lxw1sR#c|%AZT<>x2$)-z2-ZP_QIeG9#?%lRX_W zjzOcSWH{E)#~M1uu7KC~-`=Zn@Y`_kO+OBfEt}tY_bnnakuYSs#rH{Cu1oWI6`ztq zRzjUMfgyV3NqeQ})%o$LuTsadb4%HkmFyL^%M^*3OJA|J{``+{w)qbRZ7IH>4{~#8VD;9u;BfU`23>B-|%Hk@z2!ic=nTLNmMHP#%;}F%-Se zNeLU1yu_z+U$kZJnI6YhjxW<;fFWqGoqE)&m`fmI(jd~XRAw;9?Kifm_P4sEp1G~i zFIpwORjpsC5WeL#i(e%F5|B~;fvm-A4-=-Xz}|f}4k23c?LmdD8y?gZO(9XI8|3t( zYVZT=cVewiFVZCbhI`-3;S<7-ej>y+J@>&!XD)p4`RUJ29X!}>;Yy_Sz4zXJe$)E+ z_}jW=ORAWXkrTua{ZE!Hoqo~e>6FA5U$v~fBY%T}7|oIagXOCOk5eEZ#j33-rY6ut zEK@f%5oNtxY-?*&cQrSu8?9o1w?fC!t`#M&$Z~luSLX4dF)`;bVF6rLr`W@dt*vSo zG@={lg+Cuk)ppX;XS5ZL{hc3}zq)9jK-%|Xh=F$FSnZqI;N)^(Oq^N+aFJrtI~&!crm8VhsETut$5ZopbHv=- z(h_BfZrN@V#L~;KQFU`OQOcueK}s&~i}Xd4PKV-sy3Y~fqWkm+2e<4Y%x2S^{yTTB zaPHmv_FsqC8{2-4 zj}U01BhwE!SFVte963*(%s`Od_=&Rr!uAip`3A&}p;f*4zFZXiw@Db=UScha0_w-~}h24f7Anh2!k*5^pMU<=yYG;V z|5lE^Y3sJQU0F_PQe>y_J~9V~gcfN9#HN_Vh%iBHh8SaUeP;TwAjZ}xVM`aSl?7-K z5!KY#438!!)#PLoF$Zi!4E3$C7q!fIwn~f4t>IY%GJ=fgb;^Wg(=%YD?U}BwBRrrP z*wtNbmNX1|DJ|}dT*`X3h;gNE*EDO*8D5DndTC~61~ZVH(FYG7e*b?Es{!_tVX@=G zixg2qX8X}A~IXpak_wHTsMo8h83s>TV z+v9Y$6k(1=qN7W09oDYDh{)(}t0C7V+L=pLZ>6a$XyB8IzjGP-1(O&Pf)8GhB*^t( z-k?76hrb*-0BEXaga$p&}mJ55e7M)+x(Lcvo;RvC8a2MTzq~xV3 z6a%@O3x$d5%>#yv-RkiWPXrZbf3r*CO%;p9C}MCYrrOqeA*-`-uXGf@TzXm}!Z>3; zAQqwW`!h!{2A?=^VmGy)au`F>l{+!Sy@ua9e>xx0N9D5pIAC3ykkv=+B`>h+PTAlUufL|EX+TfAPofjX|QSbfzknRcgMB7N}!V!Xkv&-RZmi)1$T7g2K_GIWr`W5So7JvXNk@JJJcOHi;tT|>ED#_Q3bETMHxTESrsT;Wh7Kr+ zIh9ac2mbok6MqG?BS5Bdfw7uzz)!! zJi!%u^~TSM``A^U<=OCd26;cA(ds(=;>1I-fKTS;$2eCySocqJ9-L>>|6l+6Hy8iq zGZ15!fkc{LoW&Vt6DvA2frEOaM;4oxFA`=G<3!i^%}b+)r)R#C z?3}p-U{sAaw)}EB$=N9OrY*P+@zSlcX>Q(z46E?DNE@PyUkv5*Q*(nLHhiUjc({N1 z!Swmd*KgNm3+~w-lkoxqldcx*0V!lKOBYoh?=>?905-e8ju!{CsX&bE3IqzXV(uv~ z@pQ`m(#jZ{7FmKOoFJB0Tu1)$hre<+_e3j*(P-U9$!jT!ZkBF-xe1`B5F9(q>o#rz z)&zds%V+LS4`SB4e+ksCT?Mb(U;UzUVAG4wZQlI+j;$}f^2%%Pzs>uk-h2J|b;aH$ z?012pvg}`mP?9$Y{8F6H;Y&wX054uIFwMnn5c}tw7cKx9*CFee$jmvdW(Y|~bZf~( zwFk%~&OnXm`cG{j2IXOG2}P>XhAp|t`6VK2Wh`Xl8{N_=6lrkE(%bx(BY3G#h^>w% zwHaf)tH33U3)86AKah|3pxi9aN3qh~ym=GL_xX{L>FGzbq~Ym9m&i>Zqm}RYr8(-i z|L)DV-hw9#V3!nHXt%@0*TXH@kSI#Z!m?jcXlM9L!EA17DuT!BZbt`<=>7o{}?RC1DQr{=XG_X+aX)_nJ zGiQK|BR2r6NE954ZmUV!Vljjb2ZvY_a%R zYvXTtW#jtxmKc56Sy&|1n_4!q z{2-Ng9{?^D>1vy6WP5~H*nX%yYj~qIF2)F?3*mQqC4(-F-`jtOlzI5#dF~-#^ zy5-O6G;f_oaKk^IV??i++ludOT-Orr*PSMd*!wFACMfPE$W=)g-*5-OL;(!M_)KI1 zB0ovlz-k2T0-p3S+67hu$dIrj4v>+Ry#EAAmf_$B)q2PtkE88~K$)8=iM!eyImc_q z2;-@Nw9K&iRAZy6M&9YLwK~Q_aZSe96vIZ-`8lA3CT9LB4b;UYgv|M%lO5zWtkX5h%toADkhKw7HBJ{WOkP(-6lBMrQLMcH0ew_ zvzY7eiZ#c-3?2rK-eeaj&YWcjvBb9##V&40Fn6VUx z4tRq-iUD)5>n?TyHfv9Oz&UXlH(3T^dy&1i#+@Y~M&wmp80rs6qH2|r!<%rpUnZIN>5X z7${?#9L(5_E^{DaILQ+?PMo*_gje5t55!Q*e}D9V%8&l)d1l|D@XHiYcG|dY%a%=R z);+tLuH}y-5wB+dipv+%>2xZ_zeO{xr-Fln;u9MLu@g6#9{{s=jvYI6s7zarNSQA= zdxz=7zE-8%{Q;z_wU)RQdlWd^9|*P9sxv|nA`%gE<1}Yx3HzAKW?i0aDxKqVN%0bM z*H@5TiE)Wg?5uLFs#a~tI-Rp(UP!vLomyX!#5<5O5sB<=a}e>IgGGpCd>q1&cR%>x z+Ajg@%m4b@M>oUQz53#d8(!pV^X82kx2}20?2E4gv4RcRWU6P1a(jp zEb%%-ZROC7Bky8xDKKf>{g6Yf9(%_aRm6(z!|baAXG5=;D5$b2F$z%{1g|P+ZJehw z5w8$iAUR5j?1J@&G=ki8>eLt?koSI{45;RFjor9;#SFbVP{^h4%KDrzJ^fLvVpXh1*`hvBquJ&v~5wl4@ zn5Cv%VfMGwfAFD`q5(U^167c@Jt2ZTz9aI+9MF7iip}?l_s2#`*{T*CzOvRyEQTct z6!yYv(ubTlyNVGq)s3)ZD7^~>%cfK5v>HR{H06xj&MMNZia_&$gMnu8Y92la$!7K4 zzF7!k_#ov~8yhha4g^bo8~%L9FF*|TxOYzy>$J7Wz#aMO!%si`qiRXCucKr3x>kAC zzM5kf$PZ)*&oGROak|WHn01Z89fLU%E*;2a$bpH&gx}yUD6{x)L3>lrEaS6vgaJ4> zg%ZRAe1!rUkD~eyR|~z}J2a*iBgFX%iy>EIj!(uC>B=^xVlnpBZ#ygKRKn=R#wicD zdCs1#YObibw|pDI2ftg)L(E#(2Q}#V#!0P@2koS8=M?rIW8Z6n&yT!OM6)B{sVMIjnCg1UmMxX^*(DH%UYucQn zv_Dd4GRj&ZMvBymN}49SRtY2CMQ`3!vW=QWAG0_=>?OLHWDDsn!-ODzA%+m0&Fz^DUY=qUJsmnznL#S>HQvqG%70yk$O&wPGFf>h^nh>qBe+emdauT^O8u0_;hM+Mn|z`_1zVvF0$Y++0xWFW?( z8^fFI{{dMUv<|c@nR0qnaEDfJ!wQi?gG9P^@KP z0t@CQk^n^jo64e;K!6l22yz)(w0puf4w36I3ovcHET#2OoXv~+`cb6`Ke z`_vdSDVNTD;m~LX1J@SYBR2=%Q7U)s5vKpyAEx4i;b&hf5l)PcC&uIPQ6Uy|fD~#M z1hHTcz#svJofQ$o><9HhtM@i)#pv2e#`>#mA*D9vP^G7>0ohzD-S8o`^nOo8r1-%C ztRq=PE-|c98r@Xjhgn;!3LNqOXG3ta? zDne!0d35hCq+W<2W=Pnel+xi4qf;=NqMrtfa9wppD*-GT$&5)q)(m3FR(zxl+)guM zyAUByHS1w4KnOB$`&~*WmzvJZ9v;AUp^ZF%3o{1Eb|{2Wwl7Z=^~4mrHWKlx`OO1S zGK=e!g)ecw2*N_ePaxz_74MB3?_B-r`okl#r(b&h(<56SuLG3WwU06|Z`wl%fZN_T; zP|MeZWDKY5whOb4U;XL}0DI@ku2Dnk*eJQ%z3a%46IZWad$_Y)`*K5vE7$nK3tP8t z+r9}N#npngZY!p>+mcrLEK7imjv7)h*)mR<4)TRg%C-TbcDI3ZFT6r?@Ozp4dbL7A z?DW-CgH~(Jej9N`U2ka}Y2h+iFNjSiW0WcD%R!8>fgGZiIZr*ds5G!2UIr)>Q9In^B73*T8<8%x!NsKnV*mL5m8+LdT814>zQMkG%)aXoM9~jN zY~Aw-O*3XQ8PsC)j%|Rpy1vpguk`c`562TghV2(xJla46^KR&2^ztTV%y>x$2kFyk z^8i6WzQ3@a#mtA(CJ$)}VoEAON<~E$A_&Nkv938PNNO7-W9*0DpG&5xQ|8FTvoq%x zmb_gR{kEEx0j&N}WA!PJ1p>fdQAHiy?-pWoPCDsqjv+`07>k!pA}9ngm915YU}wi9 z1ti0}4qZNW`QeDI*)Z;=nLM4z?9oE<2V*RT80YH}o;(bRBaJi$_UJdYSTka3uK z`_DV=RLM(W@p%O-975D`Lyl1bVl>2smjc)t#I3GY_6%Q%vpmh}TP1csGMy2F27z)> z(v?=JGAFHEC@^zB$_NGBm`*Au zjZC|!j}d5qSZO~^VGzTPVfQ6zAQq^q>vPWD$LP6S5~#+Gj$9`~W)_fCOK*}^8cxSH z!>-iWpu!|e0_>82*h8C^FLkp=7Q-}DR=!^9q>cw4%(hjG_dLC6iX}exutdzV#(f4b z(F7w&d~pUHW)VhENlB%Zc+igBR`%}f@({^3^$Jt4!16mo6fh7rgV#`<^X;PgqM1mf z_`wWRrYTXWu>p`4jQN@*e1$eec8sP}UrEV)Wh@mIMX0zO z(M~N0y5tpQ$bOrbu`9b8e2|=*=6d$b48Jpa^YRA|ty~wp{K_l-WcgC@gQd4mQ}^uU zY~23LvyZ>`+jnL)?piC^ zqBtf=Qx!wAb``a~L-cjuQekWS0|zl*(no`t zWNe^j-)qbD{4b)ap_GC)imn0GYv72i$4tCJ9_OG=Y}as5V>X?6E9Q!V*mVN3hm8yu zY})?ZhL!T^h0~?I|595+7iVbO<0H0ib7c_>Dk!D)eByBfnG6-dtG>W2pBL?teEot* z4Ap$oZpA(BTlbb{>w5WxNdPkHT7@`(d_%k!y~IX>1^1Q zr8&VuiZfI=(mWso23$G{`};$-ijqBEFBz3PtX%@T%-5-0gIH8}i&DaWEX1x}eN%?t z&it{Q@5)lH$%=mS)nEMiqsu*8w!XOV)@hA(5)5tL^3>xS{*6ppPEB=pgO%k2vG{nr z4XKi`0x?;#GLv@;H@(x@YxlwEaO*aAtn{*7lW*(}va(n1tAiQ(t)jNzwTy{DL@D#dDvOHxHVaSAiWi@gD# z2x78hKz3+1x@z~hC?+wC{0D07$G*7^Vvjc5>(;D)b?auCvD(@lYaZKKKE0i#$0=m2 z+cJn*gc%cg1D1bQp+G^I5>58H2jwflFN(FjrNx}Ox{xZc8oX%XwT32=3eGkhLTN8s zHgjDfug6_xii>BmSr8LpOfHO$&Sbq+%xM%t?Ty_(`n7=!1sz}*s_u0V%VNh|IpR}k z9uHimS#?9!VD=)q(8W6Zv@sXX#xhyRS~OjC^BaQxKkn_GLe#eHsHxq&W!>W|B?s4iF(({L7nLLR9NR`ksn2 z#%u_3h%OO2L24OR? zl&5J7uue%W4Pp%@n4r5N_Bg||3Qt>OC)t;dKiQc}-~8qqI&j~oTWif81g`S&*X!r& z1zFewsgY$uGDTz)ZPLa(A+P`=#}rUV;9A#(Im z)b8G+BM2!gjS(-DVKaY!ekC*Q3MZE`ii>7aMHTsoB7Y{)PT-UC0I`T>W;`xpFX9QP z1_&tFKT@25mJ+OfZe)zcc+|@MA)Mms4D|eWxRxj~_A&LI{})fnv#S_U*}2nCT1mA! zPWT#y7#_?fxI;@pnXcmzXBr*=a$pDVcImbmtXW4imt-)#udhnZF+e#mU<0>mY#Px8 zX*e9W5Zl0TFelD(?*r%oE0?BcpsMKf^i&ehPBRqsp#L~H<=v$Dy70-sfD-yz2+c@B z&vZ7iAo0KEYpls7Dk-RS80~CYgTbt3m9aKx{8WZ7JYNCIy)*dh_N-A759V^dt_GcI@uKG{ALf2{1Ex z5)RYE`$~`WV$w?^UkWW$wV==#wfxJaT@VKdBD9iV4A{g>kVmZ51Q>Oq?pbax7agEC z6Pd5%mbrzLKglZfLS`uv*^}2uo&}ps+6YuLB&I`L7FEn(JRmh8QX)>tg}aGhJqTHj zB$EXe5MxK`%9WGY-&~B83o>1Ld(oR8f!N37Nl)Og$Azb?$04~B!(ll~WlBCFrtnq$ zByl7g+FuEovHhGf5DQP;fybznu9+>M>F7dJgbneAG?Pi`RxR1X+q%rDWJ8=a&PY9o ztxU&~nMg4k5as2IL;nY{Nf5g*GGN&sdWzKfjt>+*Sq?GX<_3m7nuX zpToEJ&mUfU@6s`gPEMyA#KNhRK6hm7gO6W+GHSVY{VNadODo|-rcdc>;64zE8YV2t zp;RG!#K($n6l6eHW=^nszNDnWxnIe5KUqyEgi7iLsC@6Tl)f9pS6UK5D;eQtt8zKR zj=nhTDg1az%^8USu?rVKOyiuWWXp(_&YkEamakmHG7V$$mH5Y~%iW_MiN*=E0nn6Q z`0cNeu@B#W|MCR8Miwm{O79-a`Yv(#Bc`ffSo0**k}0Tp0;MEXD7VMm?P>6-M#i># zl-fDZI7z9ec;R_t6jO1m()Il zxBoC&TTT_RzS295(Craoh}S2RvStGVjGozObh1+(SpZip-_UgfF&7e+Z`!-v<2jd# zWiYLR7;TWt6P6Fm64+oSrmXNs7m=}l{f?QPC!mxCJLYy0ASK-iQH*=S2@Y|rI+lw6 zeRh*is3s0QBN_6R1br7oV8m4!O3A7vFqiL%|bJ$~Bv@ zGxJQ2Wm7^-7hOuid>DUSAk`2>AyCF>(Aa#3fy`82{rB#OXS$y&oJ3O}vYGv@vmst= z0V;%)d}?Z?m$M}@WTI-`S-&z584wRYkpRn+5idfvk5SGGg)JkWpqv92Y6`jFjP6qe zycY5^S*ja!c40;rYz8vVcfet@cAu&5;4p zqZ;j^_MYD3)lf@{@r)GQH;iT=U>qRp7GfTESOw~Z&wqUh#6C5M39#Yu1ktyy2Azn@ z%51>)|KQY6b8{uoCKfHllK0(>>N^0K1!8gd9*MIpLLr9kIy!`y)X`M2(aF0EzlL<> z)!~Cb?{BVVK)9MGGeA&nV`Pr;P7T{tRq3wXIYZk$6RB*kl!z_W-|C4awZ4h&z1^xw z;|qDjF4(BQ02m3GZSNGvOC%6)_&O9T0V>j zG;QoRZ2p}6U59KcnednWLMvWB;YUT=rs8m$5HEYboP~^ooZ-gm(-m3b1fjcde&Nd%HB> z3>ywD*nj+(7(lq_j?eWUnw`l-=gkq1_$`E(AR`-_no2H$7$37)5X+rW9=hG=NEVBwtbwoKp_G_jE0&;6n5zpI>4HE+^75(Ock-0PX}aoRWjr zlJMF_45}ZMJRk82I)tDwZ0^!(Vs|3@JNCBZlvh6cla+C`EvDePbn3p|1vlSR%(!00 z$X0X)s<4oTO~E3f*h`%?m4fW84l)b|LZnYuN$<}=Or=*nEcfVi9DWR1U$nLk3bD5b zTgb!M71Ud@EQ~wqy7YM%Ppo1<5ngL}$4pBb!8X(BnF6~4CUvpS8Ew|h2XcmwK&JH) zg)r$^LWMSDiqXa80xdnV;F7i<`FUlN{t!AE`Y?PJWGPx}LxkPM(%@tsc*4W z`BK=J%V=|zZdkXXQLHrMW_=wU2m?mDIo=)+nd-LOw~rvNJ%5Vp6&hwxQ&&EzQHvw( z4=ICD$GZHU4%vm03AUX*m{kx;XeUBhY zjzQe0fn4qz-(oQ4FG3ny+_HW}$=VEmB#2IfAr5%EvzUeV~*hE=YnU{U4MJ?Y^u9&!^Xu`n~LdvRWB zmUM6*kqEqIiJ*^D1(%H&u_<^5a?Pas0H$yOV$k8R zfGxmIo;{RFGx(QEIqa;SP*2ETZGbX7O&eJ{TA{<3cBz#Yo%zNsTS}$ONn?&d?%@`Hc$f~fH`>q$#N4dH$lTM>J1;ic^H!ivj4kZ3Yzp;g?^ominSp<}TRr*rN`i&OK zS+uF4}p`Ja91{%6Jc2+})ecuZ&oRT)i+ww^*?)LboP%|j(@m5jf zNt7{ujimt`$qQ8rN@FO&s|8nFY|CV%ZYGukk_)A=dL1Yop+D?>y*4@3O~=@oibdye z=T487EFu54MZQ~eu>BwwR#ATrt(AYfiBI0W{swxEmk=Rwr&uIrGd98aKib~Xde^+i z#=l*+Qel5fKA*9(+0rfRUntp#TA|&!>qL8{=9y9F;BXBfbfR6*e3pX z_~enchY!;|L;@6378laZTefX36jE(c5-z2IyCMe;Ef8}p1vb-tDv=`e`MxJLZ`L=E zg7x(tI3UO5XcCql5>|GK62>W#{mWmJl_5RO+!EsvumCfbvs>7dOBS*lsCKItj=_20 z(+$@3ut{y^44G9RyR^iR;w)7h*6bql%%lgRX1Y400&ZDL6SYK~Q`?#1X3=n!L|;5r zWgx$h&MaAs<)98ziJlM2k`QnFfy6?zp~D>~zE7dT?D1)SN^Edj*R0<{IWC4YgS|3F z#scc*ye7F`bS7KYY_>p*GC@5B^vVMwigplF2V8aPJ8ZU`;`%(buXEutRegrz-c!RL zd8|QFvohkX*f>L^Rm?%RhVQTgN7GO^$O{_OCMv%~w^GpUTI zaM1aSWO(#%UGoBLhoI*-t=13^t48ip3e^aQX9<`9F;4M zX*o$FV|#X`NW+>Wx~aww@k{`g^)Toaj6^~NAjI#rO(bd~X(bX%0L&zRnK50K*r1#v z#B44B11F7+lrj82z=_BCjk-6DDDk$jY^a17eF7$~`~j2Kg7||-#FUCjy9@17;swhP zZ~EP?G(RwmeE~_aHi+1OY*WOxp@&-$BP-cj%Bv;6ahm`WVwSl7=ATsR2(pg-vN0tV znje562OCrC@=z)sSHoYEIMR9=)Q&E4%P>2eCfX@J%mZmhLFcOqDJ}5ZW)8$`t!y&s z23SNdCSn6Q=YuS5q^fZ@9x2H9LQoT{NaGL8EQG|m5P7WWP?bQkx6HUC`XChSBrmNH zTXVR8cA2jVBZn+M^$^6M@c7wA+aM=4Th;k!f)3)Uxh;v#+%^j*ZbLQ{a#DqLijLobr*PnJQ1I6YlMSG%?)clT>X`)#(6g ze<<&9F{0B}9!bYFuL? z0rZh+*P@MovmrqR(Phc`3fD%4*@>jL758f#d-v_Zv9b8rKvPSj77bfThA9Mfk0jhf0lBn(rT~`B-uvCx%P(-m zI|#QF^A1!tXZkY~#zS>IbzyH$=(Y9#Kf$y1SDQ;;{Q0&u&jr|;4I4{e`Q?T+FZ^Vl z-xVR7gl5bMV-9A^ny8gP$nYIw*4nP_IG@_t^hFbH8IR+aP1r`OL+YFg+9ps0+|K%Q zSfgVej*lyJq#7%8gpN?8wZ-+1+2I} z8pU*^;-oXS4PqiI7MnyC`^rMW{OhZ{z;Vh`dbX)jT=4`(H`7JaYMD?qQM&dg?3}Qf zZeVWzIZSNr+LCAF?bmGPrJJ`L*@yOqgUnFYG%)GM>ePvPrYzv)WKEDLd-{>DR{cqg z0jG>(0%C`uit8!_W{TX3v5@9@v@W78!EiXFU4Pr+TG1;!YcA*T^Vt*?0VJpR7q zij_;kLR%_7Tlb7||JwC`{Ws4cF7@m_Ca2lL_d9&tZcVSyMC;+x91K4VgV1}oIco%Nd*-S9SLT&9J7*_?H#@5wIu^s1{! zF>07qR07$a&uaWo$<*O4R4xWv#4HS2RKS8mAR@KOqVoQ*SE~zHI$oH+sxRz@7-q73mf@$FWBd}0V9gYoeP55D{cjs|Q@ zCU$Jke)=I2j@$L8HkY~qtVpr#nFv0E*eY}_)0~(;kU?fP+Pvh-OW~Q%28yselTIFo z#$=d=NS_~Bg4(W@v9YGX_&`}VlJJJ4%3`o~FJ=5@1dfsrS%ojmvy!b!Nld|?L6zT{ zwKUz;uiae*q*;A34i$R47Is}oL6p2Q$WLcH&fIV<-6#T?inBc)cX^>04HSnavJbw5 zIR#dzYYt+J9QC2!heqmNQan9j{K)t}P9@+q-dwVpfH6uC#2Q8-BV>w&MYT!b{YE~e z{>{GLN-|g$o-wiE&-NeLwa5USXa`!!Ge!3u47!l0E=CAWMZ@K;)@xiO);=HY@Pumi z?2A}r-sJjPd!wCp0l0Ko&JdPH2I6C6`}B6-jcfYCF*4`IY-uIo=S>bx3%Da5Gchs# z;QsOV#ZhwOs)z|s?b+9N0{UDU=9LFgu%a+-7h-6oxe@2vM5l7~4s)Hs1wyn*6px_$ zZ=-9Gm#hUMNpj2W+7Lha`NwE1)?|E%2n555%F_G6D>Igf0;Y*zL$0{UYu=7;K8_D?Hfo2g4plR?t`J(S-GjKc+y{-_op?ZRzQ(25{YbrCuJ26ZA%Gd1f`#{ zP@pbvqqdo+QBQHD;Yi2!WAD8G4v4+6ufD-@2hD`a@^pTvWqI6gzX4*jJg_n?>#}O8 zlC{>I;lLsrqY`NI9O94UpByCL*TP#~K8dMCyW&_FfGtmu0i3)t{uQvjE?vwr>SbMB z!KtaKiHYe8_wR__h^6MMbSdw0QE6A408%G zuL#v4Ft$0==&`eTq+MiYFEde zzX32fN(9-hZ||NYp{Dk!_2^y67=HQD&2LAA*jwtas(5hF&X{H{O_Mk0ibQDh6ka2ct>RVri*o6)BojftP?+~? zWHm6?-2CQVb-VT*|N6#_+w>nOk-gPvAV$*7fwS5JPn*)QV5A78WTYJNATK;9Y}2ba zpGby+Be58Q&v^N)GzHmsU|(>4c03X#wVJ&*-90r0X5a->j*b0zoV#;!V5|$k1X*(L zeS-d#DfE6!Ju4}XB}n+Ep-;;3BFfG)+5`cO?nz$Z*?afC`|||fKM8f>Bo@YR&Y9#Z|2!~> zdGXmLOsA*1r>|q?5T9XV4rdk;+R&3|s;0jF94bq;Z53?E;kGe$A-f_zeWE6jP9J_e+?)B@G$WGTk>D2C49gpWSbupyi%N0vkElOWD zsH8Oa%bXy9lXSx;x@b@EXVoBc%c7T=wNx937GIu&%xOfU&6K7`sBXR^D? z@yP$4e(>r2(T_%N-@q&1zj*DRKVr!4`l%gnFiz7z;6HPw^Y9~Nf759Y^S4EEIscML z!)F!4*sBC+T#a8IB7abV@I;!x%v01nkU**2apwkN#aB)Z(@Ls8m=2~E>QtQ;hbB0S zc@)uz05*nc2{OHq5T!^Y>4@ZVrrNnrou{`#KIh&zfZh3kl{QArQMXFa-wk(xk8^|= zaB-;hOeyMLAcl?J9v#IgVf4Co?dq+&qt}^Y>*$cQ@tJdH`XBLJfjJegT*|0nU0802 ztkzlkTBtgaATo8=`cfpJx}7c6WX?0EwO49_h{N0jF->$Hsp*V42I*0$w2O%pUqUh) z0Os~2gcOQX(i0p>Cd#FgFdMLVBH@dAdShU+FX|6>F8;c=p3TIX{^m#NMZQyP$J3l- zH!$fpPI81h^1}W5-;9#CUKC)*70Pa-b8^y|kymf9;@sZmM+KYqTrBO56^rDtR5wdE zZ7o?n3Yx$7k_dF+W$JTbF!1-;GZq@w?V6phfC-jr!GHet3PlW*xDNV*K@1B`3=m^i z!RKgh@mO-Bzrynb<^?m5a@8o#DG zY_fefmyiKoc0p`xs}%bwj=95iwG>y#8QAR$f*87jGnyu5Ef!T&W)yGSx_Nu_${vV9 zVCu2S!4si6W|zb~AQHN201GO8#rW6^_enY$7%5BhL8Wot`aj~%|0xP{z2Y@kMU4qK z(db3)(By~P0t;OBE;O>cZQLch(sDPu1$13WsoW|Cm_Aic3bj2O|RmH-bTUOD8`O?>-oT#n#bpwxp6+Z*y-;eB`EgQ) z5+w2D3P}$0hBtg`q~9pd)K^w6965)r38N_NWULB~oy0Jj&&l&DHTds@-7aw>JBgq7 zqq%vITA8QH=R*p^_m89ZLPy(dwE&wzCXRImMM=a512kX;{)ds)w%}Kufv+OO*WlnJ zioKIDr0iN6i;?FCF$~SwlK~lL)cCq)UD&#Z&nFh-5|gp-OlM9U< zvVO+FOsNtr*ozp8<`r;08g-_=O3{g^K}l|yA8a$tUbv{XQuMExQ#wK^`~F8q-v5JM zFYGT$Ib*w|$9+3jXPQNyFMkV7aPDIC3E_2hj+B4z?i7;LLme&k6KCq2j)~dISq6g3 z&iEn{7f0=;bKqKi-Hn!(&KnUVE?Z)Q9g~BDqewK5*;tA!0~n9=mxlWg z;q7da#(*=V2&=6Oo@ql2kr-~Ps}pC@h(|)>!(aPJ8L|3v(sri3k&}_}=DvNIY9VcbZ1dkEFhLA`)@z# zTD`8dG6peh0QLeg3^8|*P*v2(XIdkeLeL(O&|tjL|sIm7J^&u*$9RTu|9;g zBVrXBfgD?^YTmj+Hc7_g{O;ciVhE(~Mi%{G=8o-Kk@t&qbJoBLh?xhBlPX~4j%S0~ zcmm<{+qb`&qG>-Sk0h8}^-KP60dtpn$d_5!v6Dnqd4Uk)qah!V%Uz!yI#W?{qpEYG>G!kk&jw>1!Yt1Z$|&4U@#`&v?Wxf<%n4 zUL<1F8`+Xty~w}P7a5CnA|6y+65#hPH?I{sqKo1QeAxWt1u30Rd04AMK#LtK?AREHRfHbBZ*%s`4+&?0UPFcaD4hFPPoa7g6KaWE(4;Hk-!Ew$sj_%z9Fz~vrM-Ww4*Ji3+E{43;AsnsiJ0B!Q3E0+_Ml zzls-SFv7ouGO*Ft-&M}vY{F@pykNbwfEK@8NWHcivKYJa8(BgMF@EBf!c&};taC?B zQ!tW1$oi`;S5l2AKedI-)=z4B|2TKkd6V>sFscdwBj>BSx-l^`Q?iZ7)NFgAy(Wf(p@w6CzThyG@A8rK{geiOoKG;r&T}&Z2&d$)4zIl zByl>6QK+q1+jqVDx;Z0XBDOcruU@rsk1P^Xyl)csSI>@*k6!{YxmI=-r%=!;T`%x2 z7_1VrA~pQ>S=%rLI@Rt|J!LCBfR&%$|#!-2Hbo2_N4j{%4*KK7^zQ9h_ z;Ix$8XCAeEscUt`55Kuew3@J=@lzUdbaE;4|8(zmFK`JkUY$EyZq!v7z-AIv&z>c! zCJ+fNv^CAF@;4j=&?9?Lkyis4UvsB`;KS_dW&eIJ@#+z8LPn9j}at z%FX4STXRr)qM#c2yxe6Hd8RGK_V2 z`s}o%O09anQ>!j-)vozh#-l-1Yb@yOl3le>R9n77?2BO%nPqK5N@D&} zJL`k6U+}qdUFu1a#ayKu>tVTtVpb+D(oH?MnyNxAPrhQYfkiR-tX5ilt`$zoY7i^- zSc$QCmDPn7R4@pMz~>>6)hS&m!b)1t>K@J%L7&W;ECC6|1)%*qs!ktaMj6^QWOW0W z5QB&MldNqAIQBH^g&W%%RD(^@8-%3$<3j9vrLI4tESSLEbL z!JnV+4~MNHY>FEnzBxVpU^B!<3}QfbbY^CQt7f$FdASS!5HFM-vUn7{s+OKU_ofPE zXGI8sm?t2}M881sSk+knNZ%OebuoJt-oK}?V)og4fAt=u2@v}<>+d!JJDmB`Y|Hdr zIXsqxjW^y}v^_;BraMvzn2k^AnnWVIHPYd8M)iIpZ<^%lN-55{w4jrGXysVfbyk)` zZ75JWTn4bq_q%mW(hBP6Xo+?3t8Hb1T_25BJbk`Yd&2AUL3g#3RM1w=-*`$ji}ZmJ zZKQ1??#c@Z$6{&=u1Jr@hGRJNiEQRUThM(X{@#ZlN?POBzs>qsA!YzOe2}vLr&v33 zn947}pFSF0^D<+k7`rO_GGdI!3tapDlhZQdbv)5y5M(1WerPA%LoZs-M>wN&{R0T3 zaGY~d<_YuSsy%r{g+2?!T4F7+#-kG(7ei5^*b&{Bb<~lkB+_MTfUAHDD%0>pnCV*8 z2MITyk?*w_{~@v%@6{yzLg|T0TB{)re}Dk{4cx`wX8rSKh#~R~vt#R5`tA(>`V*cT zo=1tYc9Ix<8Wb_H31e6GUc$TSI*Eb{8EI=n32!K~DRUBJfdC>n?mCaJ zIAEMbM%6~b>g`s4Nuk?U<591?o=#Olm~?@Zrgq9jqX8|hnsno{yPKo!EF*sPnCVLoROXNYq+r={}YgHCb4V~%Y0+cZro6}d{gezqea2o z6SyokcZGNm`w#uB&?4LJUSmSXs{ujQ9CSMCt4}$QkF;9REmDpzr>_dljGg7TYnVJ7 zwuh&~H$m(I5oRt&z(vEcneFAP2vxgNoW^{P$r45(*O48LTa|kh-k=z}hY!p~c_udb4wrD*Z4xhjI*o4?n z-&nseV+ViK(M?8(L2{HHE zKTq4f{ZLH_hZHZL8gRaTT`>l?g%m@4I313LQtP}%Dx${XK3^f`&x>Ln;)@m2yA6nS8LpVbN((70Oaary35h z12FcaBl>(}eQA%$zkDHw`v$>A63ZdP-X+BTeCzXm+WwRc&1mI^6Df-6{bCYsKV$(g z*)R@W`mB$ZgigKIHX0ZbQ6MgAPf@$gErS??oBr?$BChKRX3@gn_4x6_yN`y27+~p_ zo>Cj-IVo{?-rm@g;=BS`u@BocEZ~?t0k1NzT)~j?4F*YPRM247IQjLXNBwt#ZX@4t zA}Wq%#;)({;R?2c0Cwomp0uq;Z0Di3Ww&2e!NRoi?YDGG2eG37#$x2I@6rf}k&;?< z-E&uai3(?JsvXEK&d-3@+%i{8PDx&8KOn+@?BQhJbpX2{$S5wFyI&w?2_RVkV(du^ zcqzNLg>Y~n$b$8VrnsHho`mRHx{0=$yQ8Ck+WYmRuVL)^Jl=YzJCuqd%Jc0HW?j?S zq1$$0vYWQ!wXIKLm$%)K4~g(Kzem27}(pnwv^= zP3z*avF&Gu%pYbu+8+zA>FI}e^)$oc|6r1Sx2P0grkYgxg1)XUc{g(<3SkU!!s&2V zF!@;>LZ2)|BXa_61P|8^RTqsH9syXOxXtUWsHttJEZtc7e`Ji(W$cr_^;zuD>s}^# zszcSO)Z1@CBuw8tK!4E^QzfM@;!EW_cgwaja3cU}O_it?!mcp2!dg#mhHnb4KLOYU zM&r!f7c(Z^5DvQ=`GY#)?50dAO$4-gEkSQJ7|m8XDLTrk9q6%D&T37B&Ya=aDJ2BP ztc4iXlW2ZdI4WJ%4cT41vPBmAnIjZ+L@PHc8{SfUz1*yk$!7Y|lXBFInHU>s1TiHO zL1jpy>`U6+%gfLjfD61{MA!q(U-tD0vB_9>H;Mnd7TPG)0{Pw-6o=oYCLAVxGqZS* zoKiy$>8$wbsx$y;n%4!X*czKJd`TATyFwrM9%W}9@wpS z?3X627>7q9t-98+h%&{PhxAL3DaxX-wlA_Si)6}U*iA;Lr~f!T{YTEp%~lA2aXISl z&{f(jxFF0tn)Gq#Wrh@&Rzr9)mF{$RJM>41BYA&p?8`fWpbw5& zWi4+3xfvXXuQJ5mayvn6W$(UBGXD4S+b<#z1zQa_iS`#KKtC>KByHT&iLk3t#TXwD zqXhzDd>%YxM(pkd4EP1u9W(i5kh*ZCm(}sv&=AGHB@mP%=@}5HZIBA_@uvt<^34)W z6G?lzY8xo~uy^K0+41!)^d;Go0t^+4>8fb0GcLwexFvskRzL{%?nF~d5< z#vZ1ijdWT9R1a|1LZBqXAjVm1kv)ZCRF#aMPAugkmg{BPo;8maV`F#VMLDe-^z2p% zLN(c5)!P2r?zalvjyLyZ;7Y z;YlW~`Lnera1#uca85ET91D0n)n3}@OUXDkph>0$UeN-R{ciU=2jqg5{(p6W--e2+ zYJk#U6p#3+sj2bNF8RcFRaD4DxBc$@F2+^Adid3Y2koCfgbt(G2;Hbybdd-&=vQ(! z>ib{j&i<>(^Ni!$T(@u-+Jbcb|-_Ld5 z*Y$kAL&+g#Sh>PTsOlaXk}}q5r*d`J+0cnli+gd-TKrAqR@#)w+R;N}NbI$vQ%PBq zamP)wAK^cFCA`ROptxAC6x*|IJF)o%tFy9>Z$gx-CmyJ%0d;z57c&Y-?(Y z=S^%hVpp%Sn;jj$GA?mxxardP6@oJi9|Q)aA8}?U*vNQIjW!Y@`Wmq0MR+l^dC;o{ z4ZK^9x+bF+L5>b83Y@a1GpU}Y&9T1}&5;tb!+VgX+tIfHmK;gMz4q)}L}V!aS?-x^ zb?dtK-(R?3<8~aDtMeD7Vt6jZzWVg-JRJUvoV+qMHX>QT5aUIO(NWPh96BS^l1V@T z3uAOiuMs-L%x!wT0e*QAB1Mtz)^#L=2cuX$*h388xvioC+F?nXa=8G+xUxtJu*2=` z-eukcH=uONxB~N5Hq#Dj*zXRRDxhGm<~148V>Jh4yTnz}NUB;Yg2L+s+D28zFY zPK*)|Bcqbx;kse{vDgYOZsE2X^OVe07^d9mCv=+?|E57ho8Zr2x5rhk4{5udU_f#soB}d zi<7_;N_-LWIhMLSGDeILQ^$vg^V51L*|b-xjgFgrqPb`RS}DTL>5MOWLH0Ap;F8tS zam->VZeDf-+n)QiJSTzObbGJ;-hkP7x4Gdi&4@cPb!tn_lbHh*X3v6wPG05`p zmgUMaB>Sa9PY`7f|pGAxxM$;QaF)X3#;V{!7DYihLlIxS8V^ktUr-H&R^}q8vUuiiHX?%g;_m=9bo^vf zlcUlrVp2vr303pbYF{w!R$dV)shynaz7NDO`Py8Vo4?m+SH-ZUf;Cwoz^*||W@5hu z2CWo-$G`<7Tm=Xe--nMR>f?zxStZV)n6~p1uK*_~^V<;H~H8I`P+N z=Kdn&_RlTair(GZROuKz$l$@$&#I^g#)!d_Eg z8+R6FSC^MjpcjXO3Na6_c=et05Mz|=BZvvI*~@@RvFwhgF6UuM461R0VG|^h;<)cK z{>#@(&%{rQRZcZ+4XhX!m0DB?gqZx^P~)+Q6*e!HrcLwDJ}YhVit(dUQ&aCHDxR*S z3vdroP`fiv+z?<^eXVHJ_vm)fom}S>qp01yb4S{+n#%_5m6af?s6WV}d?!y##uX=o z*ep558cgvi*;OILW>jq=Uu?EDd)j9bM}`yF#~qftEmd44Zezpf3t3T`BthU+E1zfy zDq5mt({qbvT9Sufq`yfo|dHZ(n3{HL$3#g8pKeMsch9-3weKI9f?u3?xbmoOnlm3b;F1G zNT9M!bU5za`9f0c%&GtFV{H5UPRv7#Qw8s=Mdh3r{kkzAz@pv&)?PCe6`bTxA4yc~ zc^a1)iikjTFB?{*Q@2ktET;LFoj9SOpBoV&T^yd5?2Xll;lo_rzFc4VY;eKs!<)=k zHBV^oIiY0P=jrZ8Yg{9Smnz5}SjVb$^W*r($FOQ%%pesSq(UE{vq=Brnl>_hirzRC zmt@)Gafq=?u^h2)oRQaKNUvyAwFgBMp4MW#zA3J09VX(;(6tW+s@u3E40J77Fpkjn zFjToD$#jI~?qac&MpoPmV4sdZchGBZX>qU~<=SJZWil9ZhTMPvWk8z046fXhxOQGsOM}?h>@>kf_WP5pIZi%= z_CRtXW*m zGUzq13@y05f9?EdOy!efL;oX|O4QdU$e(uhd%3w0VqHO+8&QZkWHw)Yx@hw1be-wO0FzHB*DmLJzqp1I${Xza zLToaRETv^v+~JkWR5cpq(pikgx-8M8N{yR zO&*Fm@SUVo^|rM$P$4nWBKKB`Fe9{Zi z8d;H;AISH6#wGMeFnS30V5C6kU084z6l~hGY0GwAPFV9z--*~O3e2L>rzloWLtVpa z*0AuV@eWOH8ft&1w0J|VSCgagnjptss;{qx7Ue`Jg!RsKdO^Xco#MUawijfMp!j4} z^CiH<857fTHnL?;aoDwAr|n=wkNjaTQ&62cRBm1Lh**`$fPjC)YSo(ynoEm_?#?$X z8#sAdI`$pX?e92-U#6lfuMuK}AltfqLGrCE{u)+-$ysCw?# zVVMK5CxzHn(QI8yU>kQWMq-5c6^P3p`U`b^49>CRl43&aHKtXDh~((EH9ua^ZM?bM z5;6yUEf^cg*xb80(D`1!et}#HM9UgsC3bR5RFGoYs%&hu8;Z**SzRZbVL~KlNHixI zhH@vuEUjp}EH)dAwmMmjRiaU!*RCw;w&7vmo&iYz-06568GP=Q>nBc{tbOZ)@SeDtl>_tnW`GZ>qwm%xN@L6y<6%iXmni?&{ zw5g6@{&BTVYB6&I`+jeKDO18qDOQH$R}Wi=vJV-&?!v++3p8R+JcU;G+60RIyZpOt z-O2~TOlnJ=v)@(|>4O-96dJM5|L_Le*n1FTQA&o@$7u%zeMBgi%@Bqh#IgoMF5{<@ zMv=k&J(sx9$`ix0kf9xB_2^3?k5OZFmG5h%s*)Vjl1w3II4qT;)3TUGq^ks-dK9F2 zUTbReyPW{TNk?%hu>%l`#pLJieo<@Ch=FXas(<9{GtA3*ln*+=CCKz*f}i%}=M5#k zV5U`E^|`_yWl3JfFp`co)r46reX<#*`u*9Qxx|kSVCL%*_BCn@2j*TJ%cJdtywL`+ zr~_gxKDW^(B2)U-RuTBR6;Q=@Rpk=YKGu$l_cRIOIV*-2C>~_!JxE^9|PHnWab-`9fr^QE<&0lrMGWHcQ zr+rtTiPA_otUDpl#O6g|V?qbcW5c>pB-oiOxNq_IN-wEDzD}r`z_kxI#keH zH3j6@wrxU8l57pOGW1PF9&U9tYpUru&?#pZMQ?SwJRd9M{VU)SVy~SXXKG?7NehHE z$r=j=ImNJn`Tc%bKqYR!k((>Sk{{u;TuzJtx%r3&G{rYclmUFI6Fvw4jCA&GIz%%S|CsrlYN85ck5G|;j3j&T z{|)Rvw4LpDQ|GznCo^rQNy*EZ(loB1kAlw@BJxsK)w(%xz!_WA!heDfvdWd2z; zcKzb<+j#u|rzfx(i%BykChb@-v+h{Bs1#y#_onDFlu)65(E9{=o28Xb0-pTq?A&Pv ze&zZ*xcp*dlBUfR+VfC;Z?DZdNu~EiU1K3V+VhE{dc@%)j_21FY-DH|@%IFt+_r6- zXumueJ%f+>PL|5HO_Pz$aJqvS;wTyJWjWKH0W!zLKKU~{XlU{LXV~kt>&J<@N9Yib zZ3dtK48Caz&3d42$2|5O!>|F&s0RYDh}qV@K6tB?-8oFnGZ20!Yco1sGBGj`o8Avz z!F}TXQ2q>)e|dsy2K15qApj3;;Qcp@zz{3vrzLeD6%xmfp+bvQy4^%oJwog!A{Y*0 zkJ$OMND+<9TB(lkp%29Jm{*3{7@{y<9LwZn6S1M)Y#u&!>g#L&>%sg2X?JKD@1)%S;DNTmDG)0$PDx)~MH@b+5sd4| zv}0lfN^)tg%s$pT`^aA)s1&Ni0%-U)G@`gOPEaJ$LvDqt3S0eEyh8yd*8*Zd_MImq z^Yds;%%@M_7+KkLgi??o&g7Bk-|x;~|DOZcpH3eB@G#{Hs*~5R?>T>P$Abr}8?vyp zVOe*wIUC(@3qz}5JEm=BjTrNiR_xAf6EVW|^MiXK8*Q!219dA~!vi2S?#Y!=x`6Aq zCzZySJhBfwxOVZKy=AYosXU>E*bC}}s)tM$A}O{VyNl$8h~g=#9E~JW{&=PGWKWL+ znd8Z1WRG+p>G^7nW175qCIWrcV3=a{KGsd8;0W4e7v1@jYf1O0h z6G=S30N}aY-+n6t;b0+-L4J%8VGG=}CN;3(*&j|ONm?X|(mGLo2qO!o__e;+)Iih9 z{^H3VaLP^^J|P%OlWbp{hI5GxcW{}P!bGcIYJqHB$+&aqeX1H$ zwtaUmF|tS3!H5JL$vbQd)O0&p`b5euk6;Igr5g3t4O^8h#d=M}Q zB7uY*QApRYOEzBi3g)06xlS3IZSaw^_2q$st*^o=Sj$6!iA+biW>Uw^K`uB*RTh#3 zK$y{&s!Q#i9a;v*KyeJjvUda8`3CvtYWe{zDbYDW#uk6@JHk@v+m+f-awm>X~Cu0_{mFIO(OYQ2VTDg?Xt=ooyR!1}<&FD9U zm~4mRYNt$;iu&lb)&%0cyD`0!Au#i%5{afvpMpxl@yXZ<|pR+w4SJR<8g2*g_j9zc4WI|?<5+CSmRhJ!j&5qUamNi>cmvMZH zmCupnQDHUyCWjIO37lj{3USola!jtDrhym|gm|)^Id8@VqJsAr3SN`SupiyGZ+;s2 zZ6j1)9@DjSGK(@YDA^;rl)s4IBM%mqW=}P5nmk}W9Rv;xE@c`;l+#M`e{kb0asrxd zzedTe3$_2X4KjxwREFKfBwjcJ9f?X|1f5yQOqn@guhc7^O=FCiaExqJW2}hJS1Usy z2rfZYuHwxv6$-#LBfuEM?N@*%{fJI)WgS7~SZvi(_YAe%i?uZ7PA5BNP~;Z0;~*y2 z@9yml2XpJ|0Wcez!o~=ee*6>NODNf+S_j*bPGeITm6ts_-V#YJqiotGp8;YJ?IEU@ zC5!T!CK$94>pFv_FJ0Wz-91nVS({$(bbB(<4Bk7CW)uU3hjXiVJFq9%?WQ$$XVxkG zwyuo9II6W64z3o;(x{Nn01`vXf=q`;5a>IRC`lt(WyPrN2H<2Z7Tj#^w7Y*C#T$v3 zqe19S5>dGjlJ)02X-jhBD-L4vWkANYd^F=CPd#bO&7r0!vnoR_2w>{Ak4=sZv=mrd zF~r)6tM@zQrHYtoEP(2mFQ1(0KZA!#`VdZg))1u`>UKGZWikr-(61*A?AE*uwI5q|%TqicyPMJ3k9zFFYs6n2>l>h)G@~?q(&TWu;rl(_mx-(mdw~ z6$}Zm`(M7rWIKSdg6I8r(C0bzI7p0aN=j%H ztj2VfzeS80{0b9)bvue5mx=wwkAcJ_V(cApA3ib!Mb={((0|4XvJ38G5@*#nL3UJr zOcJ9gCsfE`@+wcQoZqJNoFGFP_0ep(AQ;-I(1>lS4XQUahBBO7O@p!*@6-LO67*w= ziz#0Aqr86|#N_(B4z?|{Agr&>Hy1r8SVqz%zzY3GXZl?rmO&@AY#I`qAE2{93C24R z4!fjXpRrtz)e|#=05<-nP;<^Wko6<>mO{EMM<$n4ZfIkuC1liN>nH$p@J4X-vI^V8 zOvXjmK{S_(8PJ%DX^#!iAtS_;ko_gD|Bpx07Y7mV2$3 z7}*BJOJ_{CSY>k}+bU}|)uvk3G7GmR#l&PZj`;w@WJe0s^-U(sPq4ki?mmX2Y#>u zoRhX>Vx@Y_Fu0WT59m&kwortmb!n`DKg5h`S6BCeO`kUy%~PU^$e^1we?oHF{_e)(KG3$)* zl6lSO8{tc_nD1>tpE2N3ARS<^EmW!ood>j5^4cX5m^nLNn8EcWp`*8EMCALAO(s#4 z>%?%(vQJ@Y$Z7!!ct+0S3nzdTc)6Ccvwl6&_%T+GMap(IhKN<`2zP(@m4gQa*^fbN zi<>65KhJpzh1Bn`XFbC+c^q_8#Vn2ENB;rDNNqu2g&60p8;3RWjE!mk&Y{DY8IW}y z=o|2P{nFbYbBwMvXGw_72sv`f$#}X!ZC0rV;F_8Wl~A9eXR}I5$w!VHj|O5z@Jax& z3~f@^S;Fi4qy9L7xIuO-24V(=##ZB8c)W?2jEr-P2JHyhwWVSqZ@CWdVY}!b-qc?3u+n}=#l7hdkupbB5J)G=<`p7%t(f1EQ5rr`@&Ll@13CNnAtu8W>nCYq& zYHV(-EG*c}0A`BCV7#}aQ>)cWwl5z^CJJDD5(2AD^-6C<$p$%$>=x)H6AJnL zsd+T*QDh1c+-=6DB*23TPxs4PL=1`N$F|FlRk5>bJ`&M1r;_}CYxb6FHPVTu&(ywS z05g0B-wYTmyAWSH#L>PrHq}uQdE>R$4r!BE*W&Tnx_27-s!N&R6_dDWGgW_tVvxJt zw^M};+!*>&cRgV&JCo#*$upjd{x~qObfC9=Dan~sUsEBOaYtNOY)WEi)5L7KoLRC} z7lCS3>`*#_T}?t_oqLI~sVz!by?Abers4+Tt|XSuUEbB**0#2F#L#Yk>Uj|3;L7rR z{X%P8gqXL)CGmNw+gs=;@B&yC$!cQohLQf@vNjkZ>Gz?O*QDUhmg%=YeEW5@EJqB* zNG_2JDO>BynMUisAld$Kg`_Ef)l2Am^_T>i?l6H9)PexkYrsmOs5?8B#wU|_ei=>y z#0m*|p)f>r&?H-8V~bX<1yXIAmMd@R$@+?mdL&_wjx>Y;XY(| ze)2az+}etU+h3Gw5g;q5{!!x*h-`oChj4z=)U$;=NL`N-Ie#2}qc?pQ%o^$YyRb3s zI{FZ85EKwu<@76MuTit?UI<~5aW@reh^r8zt}j-W>A;8kqqc{Su(`mdb*4x28IooZ z41xqu&P$LF)oKx9Gg!@04r#)pnG9aml~Qt(;*4!u7IBg%q~x}xdc8}2|69i|#0rUI zgx8WP4tJI;ttZgfpv7-k%ko%c{^Q5CWMY5!)$!RowFrO}idAYkRkE%>=gEA0d#S8z zg5FRzZ9&1P-lrSn2{-0ip!{*j1WlgzHc-Rw>BYim^$q47=0|iPS4|c_7e6{Kz??mZ{W#bNHUnql?ncd$ zQaqMt2y+CqoGrwf^#Gih0?T9s#7K|?%?h_Xq?dkmp=}uLwUaw`t-bQItsut6qKjR9 z*;--gRX}3M3|S(OdPj~c2eyMxpCYe#c{@E$SAlwy(Z;<>KP4E53zI^<54yP#-VNcaj&m# z-~>D^@fop^anbmtqs6621_mFAVr5ZNwr^UvQ?Z?FdEHwMD$#*)f!Hktpb7X`hS{XB z(ru($EFPJ2sCwi2cOk>`I!N`zHqTX^G?b?@2z+xqgTqd1bmhOVrn8KmH6f{G5%!I?GA@i;eyHp zb_Ks5BG3XaD+V8n2W49_Gw_-LG4ZiNvfb~uRpcvKJ@-GjH?`8*w(I0@+i=?&Hnz1j zi3exLYSoCS1qap*EvvGf{TW_LZmH?ZHVmFfjI5z$)jf@&)Av6*cjIGB?9!!6@31fJ z5`gV?mOvs)fOr<(IO@j7XH@-H6b@Ws?KG3nWEhXPG!SQDHRVvcjJHA5)1RLw+v~M0 z1L7B;Wyj-n%^bm%J0wfib0onmJh<$ZsktsGj*^PIxyDJyZ*U^4!m$%k!Rj6+{U`HPiS7l$I7Owi5Opk zOnk%*8|<*lR?N^U^1CGZceMc-nC;s2+)uy1)n4iszPMA%lL%8APny1`T5+kWyfQ%& z2gEEm1xZDfkLHHRj9uRK8j{pV)m^&&K8RhE@W<{Qa9BAqq`4SCb7w@mF)a>WUtb9! zhUe$am$CugJrhY3BGLw>En%)_=w9Hih8b4xulB@egn;M*2gL9hSXm*+iK?UR4xq(K zB%iVs6KRYj`LQOts_^z_oWe-lXQ~oRaB+8mS8@=%IBUC}d*XXr+|KiZXR~*(rdmnZ zG^iGWSWRx9YiU_dIRvc0YzWn2($$y0y!3kf1#d;ha*-yCOXYvO=*-hO=QYsPD{ zQKV(vYR+rN$T&hwqoo~E`k$_l8Kpm(Tyv(t0Wq_jlLF|fx&k!yJ=EfAW?*N8mP1U5 z%#+RcQe`MDyQ{})qWl#uLOd*Rb%Av^X-p3?E5RG&)NFIgKTK1QLB1OWe}@WN-BRFjUl=Kd*@nCub)0W^rbe6p|g4A zCPsGY>r2e)w5O#d>EQ;pm?`B9^FfGw zDuURU<)QJAhV0P2&7t`ucTv3q`bk`i%$g=s_jQExP(~S-Rj<{uVr0ZgtXo-f3L{uE z&?efG;b=7O>5&iHxO|x`+}g0NqF<(_MW-I0j?95u3usvoauSC?o4#=SnT>3$gW&DpY@%t_(;Jj69{Co zvP6bEm?Fqw?eNYJM3ua2lC)vG9TMOdj%j5G8-d0;viQ;DvCqdP@eg3Cz%ga75g%7d zy{+LQ>NbJ<8#pnJW!N<5K_=z*mh`KroW~V_N^;KAvM@C!#PIkCJ@uVz8{URO>ed^@ z=yj#+5o3R7EU=L#CZ^&>c1Jm$mbrVYa8?Nr&&TK2&%Jt}>kKNw2qK=iaz!@w4R~qx z16h5vjM)YG&H1{IP&uk@-y=;gru?a!L^As8-nrAg7U-0MZfq<=oJsTLPMf?;VDoNc zqsJ4QTAz1u#e|s<8~=9Jnz>ZxvF**#HbaVYLmsxC4H>rW4;Pk>PU^AfzcqvZjOIxT zdDb^Wlr_(t(_+uQT_1#|3FUGtF9n)9u`)I9IGija$B&6&VE`5k zr=nr=G%-|qr#GL>Bs8lQ0Y-0cHl$bWo}3x&<~v*lD)h#45>KDwWRfGW%Sv{AJQxh zvXRW#t}L_;FEqH4J_808U?_tdxjLtDbr75Ow7t66%h*#>#` z)Roy1+{6oH8GTe;%8W}T%P?V1bXw{B-grb_KN(3x$lO;95TkhAv(eD##Ok_hoYSPF zSPqz9Ab@a%DGy1y%Ba;L7!cSJKUZjYhxISbz`_zLjFLz5N7X(MqS{0adFt}8G@jTm zwXkq+3v8(cXrILPaQbU9Rx6G zZenAnu3ftVdIvT7LEm(1+sixtc=piE#caGn3{O&V5`H1a-=EPi9~lbb{Bl`wx`MJL zQmfJFUZX-AJK?!S6P?&a&rlLxfg^7swv>ckIo&QDQtb1QgteUly;DE!p`et@MVFS~ z`7U@f3?vm*Z8ts9R_vcnn-B~=ocyZ6-#WSoSJ#q7$_pg6*8>v6-Rin{R3kCKb<{cfe zjI7V3#CybzeK;va+`KAAfabKngvsO298GkzhdW7GR)`V}#o+4b7YYAX5 z(j^Av0RqVU;W*N(KKfZ=Fse|m9(x45b?CCo|)^>n(Lw5t&Sco>m#wu5`w|8F$Lsj#qbIWV5vQcL7wl3vM zxc@T*N!ph#Jx-JwXvklKA+ibh(v`)LdW_mgIPB6G79+Lvf&tMIm_<8H8n5%2AT}0B zQ0}03?xWvu1LnH1%Gn65=aMtn7=9n%6efZ$iCFseqAA|M7LU4Btbxm892yW}bfegL zSy+cF!Y}Zr%yPxf8r;dL(Q~8xcxBhx$jEyiEifR>rF>{>vYvXObr#G9+lJ9A+EhK! zn7fx|oVGIskD6a!AKtrLYh~o=k&irqL^ZiY29cOwJAa^WkV`oX<9T&A+;%s^Z&@CF zss|X@c)v~(3QGbe>@Sz10}rnSkbuMU>u9^sIhTZYX$DbMXqFGqGK~aR-h7) zp}`SC=kMHEiIIf0=%A9|`0}!)<+??zPz3|##8V2ooIV2(TWMvZx)S`m9&lFgL0=!@ zm|H85vDuwH_)PkRP&!KMjqJd{IAc?Td)OJHbDZ|zizlG3EJ*%l&mP9skpBiQS`7y$ zCibF?c!Gh!y5;k;rjDHAjKlrGjO2SN2t;bZc11{(CNoYhv62o+%a~as&gxJjepGZ{ zfHixe`ZG$cgz@}1^?aNm99yfJ???;|x&^6^RjIW{Qk|&;?=jsL1mF~3dO)mg?Lprf zaD8-7IfVP$AA5*m4?o)0I(vsHK3eNH?JOy3YDnD4eV@+jObt#rk0e2)7Ot_&@6x5u zFfjt8LG8#I0kshd%|h zqZqSZ4NR@HIYI#}CueUoWqgsbhjz&OAmzG_`u@D6MsPr znHlrPHhRjLtq3zwZ< zz6#+(s0Yg%eMRa?4qo>W%%t7znSb-p1>WwQTpQ+N*NfFm*;jJ!+kb(~TqHGrZZAaD zk&C~*czpNnGbaw8xQQPlL;^8^1!ApGo?*Va2epf#ZEP7b9xMQqlUO8zSvu-UlH5^_ ztG!SFGj3+Y?Vh%l&YrDa#3)=c(7k?b0TL2UL`m?WY-v+PN^Rpu?WN166w-UJ-nb{Y!V-mTD zCYRKuLb9I^zH~$hu}o18DHDOyVmV3mH&r%iU1=Dx`=nLXXPxO675f)L%g?c~X37Ug z76cL{ua)GilOtQ!wPf7n^0|RZ0p}R^Q`D_M#o)tJ=te;G*B(6R)9U-HAN=DV{&4x} zA3U7>!B0H##E+P8`sq)fdg=uw!&e>z=!K@Co?OjRvOU!~?>V>kOxN))S`g8Mf9)lh z#RJlCx(6T%g5bvfQ@QW}MywWiFh9H={%}qtt!GX`5o>yoyMmB^zTMJxrL!7H@MS2V3RBn3im1XT_}kN8iNerU;!? z#$`nik#tJ1_#-l6kpd-MT}mC5Vad$_nPpHZ1BjYQ0xbnzpYwxo0c+El0qkVZK`dG_ zF+~uwm+|}q=M>pZ)p#Q-Hh^htZgfi4zKiS*KJ4DT-~EnNOz)n3@uv@iS&)AnK-IRp zyL(XDI<0HneLL1R81uozGAsf3{0M;M2G6{KdA*Lf@bRuaFLpu2ZeGI1zS8!TSuNd} z9n^hf=1&Se8f5D;)X+{kw*>ScCa0t>XmAP^-LOreRTN_glZLiEKxNBrvZ~GG{}g7?*P5Aa0+K5?#{aw)2=*>p_fdaACo!-2t+C7N6rv9vxS*Ubm7uk47QH zNOAiG-O3>2C>~odWNVzTKOc$uQ!dxOgtHEj24b~P!+&L-K@cl9A+s&*G;@T2Mn^-Y zD!+D)r(=Pdt z)P$$4Ic!oFAcmPSayT{GQmSd3aosQ&Lps+zNXB)oPqc#7zDQ;M{crx^v%jYQ=Hcu| ze0t*c1WTGGcAyP~5@L6sU0mvKSIU7h_NViI{_^sRPEWAo_umj)oGX|ZH4!38*ciqs zeu04zD>42(yS%xXtz~1`8p*@58Rc2TxKw;qPFZFJDa#bx2_cqJd8q^hS;l38RXVc# zG~oI>Qyr=O^V2EUJ_6GjJ+w3M3+5Vv*jQARRp(NU!N<f zy3y4^cIY2}|N9S70{HD;|IPOvnwZ10Pm3aqFz5(k%oIg=B`-MVPoM1j^W~EtA7aHh zvPfqxo;dN+i?2h(u6%Rl+Gn`2Z?G}!s!3lJE8)?`M!B1tb@F|ca?Q&zqhVK)a9WR^ zQW(voPc#T(V36z-U+|eKK$h%cm3SyG_Q7&CVcE%+5b|qE$ijb}MoKmzn88}b^M7eO zx8)|!GmJJ3(3E5*6G$d)55ho5BFW}&G*V()NJg%ZVw$S8Ya3HbmKs4*D{<1Kz!ViZ zs!0MghGI4ePS`|-Fa{+Mx^dft1jtyDH?oXuNubN=dVc4i7Z80T?`D``=9%|j*q2|jpPRn*&rcBRe(TjY_o9$~^7P)l_%K}9X=u?e0N5T8FZ0Rd$&iN4 z{4zh_2*Rr1t1f7zzCZ)klA( zdbhx&{>Rr}0x{)|F?*b_j1ntOUxG@(8A30|e0F|b6H-+pIQa5&`=9r;;%LUlMY}37 zlT)&=@9v;gxaz?*nT!m@ldn+ji{Ze&{}b^$A>Y`lLtam%DC*IsitY zic86_LX6l7xu>EQqzh`pQSjGn{!9OUNxn$OG$hqaDF88SSI80=;GC*OAd4wQRM^qR zd|f<|=&(TGprW_P5mjNe#+qGjzb_w_fcENZ#1^`bxUfkOyLl4}ql2`J*i;6G zGH9mb#B{z2(P(3~NMIoY(}}Q>@H;mCUI8)g7KIs{8-vA|R2zw+tH4{ykW;nDaSAbz zqazUC)|O0kFc72>QF_ELTELhdOS5Y!hlP2tu~5&Bu!BdrnpNu5OoIZZT>BxK4#R{m24HGzylx=-KGh5U!yJ~ZDqCns+}wnGli zbqr;eLnFjzf}D!EnN~0nYvy$D?lFC5LE9VeP0WvxIbFq=t3y3*_zCQ;KsN@4nc>j6 zjxEZnNxA#_8sgDpTQqI*18OJCD`qMhc5{X!IMctQl{DOQ2hB=fH_4HY1(Yx?TwK_G zv}e6Thp+$`BsOi8kFtQ`)_kVqI z7%v}`ZOM=22gz7;H>s?{Z`WN~Y?A5`J}Ty9(5?&j&4~FQNRD_6hF zgI7-v>c{Pyo=0=;8`NUJ3%>2AH?FJ~-`JBIhVFWpcbbrep?Y=5L=3rH-~uqk`R0** z)IlkFpi%Lk3n0dQ6k@k; zpUh;)>^KD8V8j^=WmLiF&j8^-P7ITc1zb9V=RArrdxUBHE=>(|QQrUXKuZjdMc89h484ft)o0V7rYnH1lDcfZm#t55&MOgz!ka z1R+68fMH`yr$=i?CjeX|6)lWRMD57e(Hz8g|#z~0jfR>_GdHl?jqkEZ|OsaN5 zY|uRH$O}K8=<{{$cC|>ofzaNnWlIGUAckvWNlb`w{{rrLCahuIqBvh+sJqf+fYbn! zmB(${V5XDXwI9=eJ{TLJYuk zF9%!oaH@j{_eVLG)lR5IVd{&PT_SFP9zaapxOxr*J(w7&l+oAlY$HR9>WMUJv%x?e zKgBStBj*>3QEn;8fEoEEVFfQ{BucX;5itBHBpHq-{!z*1E7fvkTc@Ljo#s95cLD6m zSBOIb*f}_TA&o{MX758R)YnZDY4uOW5At6@P;CNs3BVYyz}aJ5)cmq!jWrR4VPUFJ z8P<-AQ|Mo9qj~crl}abRcmHN*6v$#C>ecJ#Pv6K4?i^mTs(uumE55(}DU=g$UgvNb;q|^TtcH1|rYX0q zPK@$FQRQRUy&Wa41L=4W=<S~L<#n(}lTA{J{p8iDaNnT{rTE2p94w;9kgmMFVI zaBYao?IEgPf;z#E)M-83*T9cZ+5ZaswDy9`>YDD z3lwVS8=IC`hr4I95tY#6T^>z;FurFV~vk5h3y0t@o!8Qsax429YwxSGkM5+= z*j@GY`r*YvzXKn3`7(GNfB%f>^(F3m@@3dlcZME%@PYc>%k>wpg4oHEsArLGhX&z$ z(VV(c#h5xec=9ZORq)mDWjZ{Yd+5J~J<7#}STHzL)tb2!QY;o;I=z$@7l<(d7B2#O zOfyYQL@0PdZl_GkSQ1P*E~btUGygW+aom_0U04ZUqh1U1Dm1tthIJOcr)%88FEq|AwA(`_8ScJxoH?>-^v;In0wGDW=G1^4-fP%TXo9!T=n5 zgcV!GG_s){vnRrc3%7SzQuFy*LE)HyBCYhgEFB<5!pQo3d>c^(?RJ9+R*KIY+@lYi z$^+$c>O=~#%rh;6<985?C-Gk@U=w2`y^z;2;gd*!R{)AYH)~S{s|JhYcEpiat{gco z?t4t^+(k_6{89ATu`wam%C<H?Wqgk~i*dx*igsa60V$XKw zy6;J|Xr@)gNamr;e{IsF7=@TtVmi8T?Fi4*9zVO+Y93hSoFxRl9AG@WK0Oi~(_SE3ZoqIkDNfT`HT7`uy|+{h7#geJy6 zdF!2n#}AHjaAL^UzI|FMeMHyl`)q#f{(8k1YoQN5dV1IdET6A^YhLs%$f5akS&Kz; z;JI8e$mfD#s;>a1R%H**lIj>ncWhVD756rG!A}$wj|1f}i4U_~=c>}kBpJ$P)o=4* zB8^L`Nv5a{Vq{?l4`nLX>DQCVM2=f<_s4ONe`?rB7n-y6;|dFK-^2$cl(F4dM` z*H_bkR&Q*slN&>cqT(R4)10+53PJ+RJdNGzl(}{}!AgQ+PO?fXH{DGpRd#al$F)6}gzk;>d0{h`|T04}omNi@}+{q@#fdWo;0nIyFKc z7b!IJnm8fW{BZ9|5(8SJ%VH?pymdy~$w#M)Xh>ml~WW%`K$TROp>-BJk$* z=1j^8hiAycB%tJcLjQ*tZ>6&Z$ht7GX0a%$mI!)G;MfZVR^M24BJ46_E}JhLOnr-3 z(6f0*k0*5k#AavTJw_BeF)=b0O^+pR$;>EVgI+Y9e&GqbX~~D);;s)&;I-fgU6eeuQO z`|q@A6siP=ICIwa2OeBCTk@|lyvTwcs#{$2v zu?(unwi8S(Y+zipH5jEje(1!J8d2}LP}|;0Tcpkzly+nRi*)mWXs^{6P)>{vE+ES= zQK?64A0wk5vQD^f?l9$RU_$qIXZ1R+YUz})5@mZ!V|`oWP%$5@T)A@P=FNF}kCrGq z-+l4bRo;a+?<_82TKX*$yCb>KcV0h#X=eiwbv@kh%$KYn!gm4GpuVQUP%B4M0jvMH z$M@ci{TNcpm=71`XtJ$Sa7T2QK%coRb~f2gov1!a?jqv7Bb#+I;qTmblq6#gWH9F( zg^HvDkz41D&>cRgVGa)7tU(cM(j-+Z6Fj}Bzi8e{syX^lp=w%+6{p1Py+`$>5)9>~{eNMK;dYhRe1ofrkMoc36y zg1$JC_S!GaQt3o2@YLRyFdqNs44cut8n?v8mT3AAc@pO%Wo2UYnG)&Ba+y*dZyZTr zHn^R|z8us*VOs!Mmrh{~Z59r-9;KlTsZds#Srzbz*fgCsqUbryD z6>rPFuEP=;2&wtEE#gv*m=HEEY}p>{e)s#BwTdUrQ~v!mNxTB5_nyW*ReAx#12-; z(DuF5P)iw>hAxmja)VfrYAe`4CG~H03o(eaeDB}lw9?-AaDECoxT&Erm{{R(ED&H= zz-kS|gjg&zz$_=dXR;z$*Li&YJMWykef!+dK!%x>h?-mQa?|8*m{v>@<@Q(%#GIr` zdyE)jh+p2qd!r3!C=_fYwV%#LA!;?qXgcJ&eo~mC$_=Mc3 zTI`Gb^WfP7Nx~A2p`b=CPT3(zaAL~$m?aQZL|4J*^V7$65>f=cgc!awP3^kY=qEQP z0qoN2moJ~212Pu2(`E>S?2W({i!LmT-JTmbdEtx!*n2WC96qEiJ_D{8g+(c%0hJ!}Lf+ZAKh;imP+Bx*jWdNf~Xe?Bi z;J|d7ihAQZ5qZX-*B|g(W1(0m%mf+>@)J+Nx!%Kryw=r3UA&@muOv+sP|Me>OlP<7 z$HfBTmHZTj311H67N8RZF`S}`? zHRT!yQE+ipicRA|E=w>N;30JS#shx8vlY=nX$#ym=-^|+c;N_zbDd)vvkEx#-K#4agX~?&`nH(&4Em1IlBLK|P2R3Hxsrsnji0`I z_5dn_1_@t9wH*mDsK1`Wy5o7)C?criCimvy^E;h5{9qJY;~Tz?M~NBEyz>qQB%Pbt zIV2AzLNha<#jhE@xee9m+=X{;ymjL}mML79)V&LqE}%8ed8yH`r&W2)@>7879G8(BVKEF1jjST z2(DQff=oXcr>bS z$0sMbk^+qTcKy#YbAO)Ef%P{BBoafA%F@XTAjWm1n>w2r#=Q6t8BHvZ+iI_qPZ?}gK{nG2}-jZRJuTxRPk7WVdgpU!Fw);NX=0`De(aWT(bxNw2)^SO_Jae|=Rt{Vwr!&X>b zoc>-PkxS4YMYqc0YP49mlh!~Y*6L%BcQ5x!eLs*0-s*3wFpFI%NS34$*MyH#fc=Gz zT_i)w7W)bJvv@p*bQIJj;Wr%=tv;~XlBjlw#iQwFy_$T5L4Xr$PkJ;VNz0AD+6`h{ z?OqI+W0e&vsz<|q>Wxc8S@0NcT%XIu(_cy;LL=#;1n=Oc^VtN2;u$P%G#sAcIzraX zi~-z4PUMd^e*1{>_-ie$zD5gSaN}rVGM4cBy(l>BVUF~|Qz-^!RJLG48xT|~H4emQ zQz;?l56TLrSC+FNwoJP0r6A;ZbQbB%8YDzH?m|-u0AV}n2q@A^(Bd0l(@5%m9)gWW z>>NZDgqkA7Y5jH6TuQ7=ZY;Xpr?3M1di%zWyVqy04muoh3QGmr$%9%EnxpW=e4Q~G z%bGp%AGE2MGn0`)2ak!EOOnrYgm249Ue!16A{et+LS#v4pFil>%PQG79+MmM`i#<8 z{$52vQ!`^3V3l&}-dHEcty|kvv&)X`batESX~k?~R;bH-?*aFcoYHMZFx-^)AY#@o ze@_G}Wrck_1^kJk8Hm-QOq!RWQT2JCN*gZ=UMxD!eT;tcA+;OQp4WK;uYWWaj+3g^ z(3^a;RNXZ2?+OwG8ra;!ZQ=GTn@eLw%+^42#Y79)=W4X(eeOi>;_UmT(2s7=UpnI|HkOwVm&RnDih$#AGdBeO0|KRl6q1YLv^< z#@y(Y=?^)R^8iNS=B;1;62OM+2~-gZ9W_0+mV?Hkm~sgXRL10(v+yd_)(>+u5ysLi z_C{vS0tp`pXgB3HGJ2-~Ys9`R05;zj3fU*4zOLS9nNiv^`oQxEt>BjoCMy*QEVA4F zV7H7+E+!On`gK-|4t^uPWdUMrnSn6D zYA=Tyb)XumM}I|3XP$b!0Q>mk8z0RKH%5SG328653u`uFU-KHd8}5QOr!j<(3Np?g z3FwLvqbi$XvW&W9L=Os1#LfXO99bW*VQAfLz45&ZVjufqf#HDjr%D{qEF{voP zLLoT=MNYn6CW|4Ll%W!?8$%4p&g_I2T|0!eM@7ciA9b_lHwqvLDN%jBc?_qCoi$xii3y? z#GHw6NHIM{9rA$)j-^|Mra;9CS}xv7yPdDR0WN2ETmLG)f3KEhJTGoegtQXO$oq;S z{Ru56p`X3cjyK@3#N_kDX?QI5|4ZWo#B^-@WB1JJLauET+!D)3l0nvEY>c=`tWgsT zwF)tga9A8$t`}e8!3rbESFT+A5WpY-{AQ=64PX=pw!JHP#2_CY*@7|kA~;i;5* z9CSsnmdb$*)TRWm})V+78;FwMqp=}%V=F(TzSZ){No?Fk>Dj>4on%;vh5j5 zc7EhRZOlw^x8?yV_?Zq~5TT-g=>(mwNxse^!6{F6u%|MAA3F1_)eFy*dugBrtqjkh+@mqjl%nQW*vCglL1RdgG+dY*wtB}bW7 zXZJ1UI|JcBj8d3Bxxp0bI6l8sZVap}eL_s`e=HP{CTV2d#qF@MdC)@ zs^}*b}+|P$hslaNza^*qUCU=D> zHhpl4JX$Hyq7~OiP{l&Feq;m*Jv8*L(bT>|OIuvbH{kn1ki0NEYzdEG`v|%EET+OAf4We!yZMjdRF5pV^yj z>`%U+viP7>Xeu@IV2f$qn}9RLyG=;Ld&9j9g>f3rwns2C(S;GMwX-Ton6$2#UiP6 zThlt;>qK%NggddSkbX+;=XjE!&*}scGfiILxp=Y?()D6ZIfx~MSk&Sg-nooya>Bp@ zf!wWo%Fv6?>`l4Yg$B< zB$5iimTUw{i0Tk&=-`>lW+d&*<)-@}Rv?n7M~xB0YHCUlaz;8vPL0%$(4pnf(*`gQ z<4VHzh3sdy1|z<E>97N+bFOP@T<{r@!~LJyAP)Dis%GxUfpR$h+-}1ocX?|E>|!9wxRJ zYqW$=O{`EiYoW=cf+2Y}ux+kOZ5N$Xq7ru=hVc@7{|buCW?KjD3TAMr&|K{3!O?4P z9AEJJxG>mESeQrvQ#*jYjfws4I>9Jaz+k#AUd*!&60Sv6lJ1ITRcq~s56)-l)(*V>2C$2xA z=Rql(i6@A`Q2E!ERh>n8X!BFXl~hyvE$-x9gf&JfG(^%{-XbEao)2L2aCm3;x0A`Y z5U#LninA_dt;#VDmyMc!jzhCeJrJXm;Tp;eaqKQ~^wIn5moup1kM1}9=sQpB!KHjL z?T48bhi)a@2EBFUAl6B%q|sT6;Q}xQ>s_IMvvryI1yAzeOYC{5YVSOYaG%GsVT2e< zO$PUn`WL+=$I3E1a&t{n^UBfcyc^;ZO9`7%u9wfR7h*@REiS&fF!1YN1KDeTf{zKa zkUL4MjNr;?MMkFBQ(u3m^YEc|o*1$SIRZs=faU?Qdi9Gd@Z$wot`(iVbS@2Iu|Rmt zHI|Vgz|g=D3A)QNI=Ix-B~HCVl7c_li);D#_k;07E)@)_IbBx=6Ve}SOPINYb~1H|;vt?JZrO#m}8#&u;zQk&*%Zq}4hmMJ};L1*qcsw-KYKPtk0GfK~5 z`EsBGT)c>dO>!fr*j4q~Z!VvQ`y~?_wgj|z1~cDY`KqVBMH6&LZ}HA-FvQvpV4JqX zEqYXJOZWKkh)+a&61WB_YdG})1jZ())MjSq7!8%U$<@JokB%CJJO_mFrnYBy&%k){ zJJ0)byf(pLye_V$!l>1%YFsF+f*oy|VJzDF*dt2@`1_iunSJ zupg-0_qs@>5Nd)*4WU~1k;$1AcE)Kd!ox3!DOPA}FZ8i)kJYrVP5tH)!MuJ{avz4`CoKfEkQigJ?-`FtW5le^t0MV{$*s(p}_doGtr++X-*--n0{ltIdYKmp_Gy@j44RH3W%Gtal+{pt~ z?WEUiKt&)%`U2dMf+%{bE0UGMsk^t7FO?xOqWT0cg}mD#mTwLFXn_p#!ob7=7!Y8x zu}en&{a<89qv15}2oFJvv?p1$@7=0b4mZFM1+l{(?Y;6avQs7bi}nL_2&JvX;GL-6 zIDdJv7zQT9=4MoMq;D6FqlW61Y*&TQN2HqFb5_3fB3MO&B(`z&_!9m`CHtRc?4LEAcrr+ zuARDwc`dRs6s!h7OQ2l_Fd~aNa_U$r?Sq0bSXu_LeJ{PcZ)aH0LyLGnw#=yNelk=h zg0Xd+`BFi&wTMJQ8k-8V=ce=beWUFDOA zpGrU&WpBmGL&L0+LO@H1jfSnxRD|kzIhhpuyz<_4+io(`MV2Z5NuUA@D3^UlJ60OPmp?EG6ily^Ux5JQ50mk6Iyb925HuAT^v z%)YgQ*l~|?GY=Az$jXvytaICl7f@JB=vJUe z5U^|6`6s{q3_lylqCpDoeT`u|S%u3rFu(`M)N9sKx1AP7cPR9D>;4;Lby zXT77NaId<9f=`xv-JK_Xc~29%-tyAL56gLxUa%&1Nan?V5^RQ$2t}cQ zf1BO{vVkqSlrMGunr5AjfHJ0)a19?Msf8kKY}Av_AX zKqu1O+{G1L#`8Ck2~gN6mq&~m5A1s(mg%#|LuR=3sA`if)?+ zydXI;eS*pu!{;PPI=lfCk@A~MX2)uT8F4SLVPsrDiUl+`1sG>$Lb06}8*H*MxX<8) zxzPLoigMeJzP{TH;uXkWmAGm7vCIPf zx}PEF-P!Pp6hR6#>e!(X=nX~~hcl*diVuq`g?qY}%8crv`a&>NRFP5o!d)K%t}~y1 z{@%w*k7h<`7pZa{0fM$=w0!GZI&qjUQ6U2~5POwy%&1`;-Vb75NqE@HbOZefpE!xu zSSm;TdS+(!uW?0qG?$BjXFy-!|@{^)7@Y748dsE`Ss65(ZPrmyQf#knpiZ`5RXLBX(y!K}q`XwTP~eT3rHQ#gY)r0S z?Q~$|mIu9VBDXwMI#OS^Tk{vxu6MN$Tjl!t<>~F>^t6k8QS&FK2Bsu*1n(~+x`XIM z{!|LY;^@=K8Sm^U`#G4=FH)5cS`>{tGm|krlBO zGhov2NgFiI6mF0fc{mK!fP;zU2Ga?w44ZvqcK3i-521gP_;KI;#_krh9Dapb>BDJ9 z1v;hTijz}3`NC_Kss#Z*MgB%VHCg9Y9MVoFH9p`HVkp9+*4?>rz*Z)9Wfy&~cZ(8V z`q=5!EG+Yt?T0Hkv3eVjjW8AS!JXfae{y?_wQ;v+xsv?PN7t`^&WQC#2%5?3OUtr* zu&vk8wF%|6y5*oIQTJiUn&g$J4Ha3redhBaBe+H6FCkzOgg|~r~TSN)3+a~Q(U*|xmgaKwTEm8Jwm;Lg4iDnuYcyu ztc({6M%v~x^vrHtzx&xlm~O4l5P%amryt6#HqM!+IeZf(7^b(4R zYwioyc?DP&jnR($NIkDWJ&-LPJ^kha*YOPW!;_b_bBS{U$UfuRkEZeXKqj*RF<#o5 zYE$RGwjcUAn~)d}(AC_>J|sq*^j@x)6u-FJQhyO6&n04sbPOs6mAs6R2xD$UcRAJH zBmJkB7$qY(F_~x;Dn0$dx4-t}E)aYF{Uaaz>fyDAzxwT!4{5YcO)(nH7y57XM-Ndok!l>53sMf%{-^I1s z!=}`~yxzs0(Y&6~ytv)PsMf%|-o&xkz?;syyWhr;$+nlxx|GYe=kChl@5}A+%=Gxu z)8)bU^v=-d#h1~%l+C%8&%3D7zPsSYy4}T^)4Z|V$EMP}rq{xq)V{Ua!lKi^x7fhA z-Nc~Ly|drS#^A@u;>nuSzpK>1m(IAZ+{LEWzrNtfxZA^<*1@gTyuIDTqt3d!+ry>M zyvgOuu-?a)(7BGvx`xQMnA5(V*ucNw#j@4Cp3u9%lh44K z(Y?*(%gW=%s@lf7*}%8ey}{$hvfaXo%(<%9#KYyx*Y3%&)4RXg!LixIkw;>GLs&%58v-Sf=&`ONV5$l>hBw%*9U<;%U-zUlMD==j$6`_{zi(5}$AzvR-k z=+v;)z_{AT#M{2q@YKrWz~Jb<+33dk^u)R2$oA~N^!whg-p#4q%a_*3qu$5r?7zk8 z*vjzIj?%)?+?ZM60xxDY@>H6n-#mIrbu9C^Lr_Z;K z$FhvXu#U;KuGhn^*~N*&vz*PjqSCvX(7LJD!K~WDpwGFI%eSf3!LHlLtk=P#*150_x-^@yiWYFQP&_CJy zUjP8y8A(JzRCr$0(8&seFcgMSGu=c3Mk-AuVtxOYiSy8Lc$2DFqzem$ZhE+@3;*VG zCPGlg_yJfjFb$$(NZvwRNUhe4L(6I&n`=lIiL6)O1aC_tX?Z+m)^s3rR>qLc%~0S{F;` zVj*D~FpCjL7Dx!p63RA9Y`_H5EOu;nxv(iJ-mqg=IjNpF`CuRjqXm?!|76ZfS0*WU zS6%~Nz>@Cop65B|dGrG?DC|gdk><;E4yn$s^&98c`bI8ayx2MxuseKIE^AB6HQcmC zU3{=}u>I=QxmPd0Rm-Wmyoq8MuFvguJ7^ir<7u_pg}ZlG=H~kP8it1(rY=u)UrYp? z0fnN(W50Io+Lt)jpFMv3?AfzVyBjw)8XFsjS02uNeCy7G(a~1&w~7*l!ej1qQxw%y zD3l4BLux@roe3Rqn#IBKcJtWs*m7$k5wP102D{x;p=iN>Yi%8EY;5R$bm#GluT1hi z%U(zw`uy{~@~y3{t36*`#n~z^@89zI+&=wcN_ecUSXn68C`Ny^r)P`VqV_Lr5?b(2x0SQVG*SE*FdrOBluKSuV8pTG6yF_j9}%#%rt{x<*o+SJ75>z`grB#gD* zI)+MHG8aMuOS4j0%-Pwgs($(MK*FV%979nm6{x9Hi%}h4tF_M0uibw*H`UlMJP#C8 z-QA;O_JAT_SJ=nKu6<2_y?BQ6U}Jf-Lb0*Y($a8$?)=BME?*oam)*Ki;t41^Trszb zVzZ)xxS3PL;S_T?YWyJ{|3W$&S&odgCPrHnk;tr~q@+Z#k!Wqjqck-1O~zzJOlh00Ck{rwmE`}={ce@pLUI%C3P$Ls6k@q#AJ`1JZM#z*Z#Vqir+ z4$rq|@u%$zT$O{4rt+*XDx92Lq~Cn&=RYQPLN2x5Uu{~X09!oeQ2`4BeT1)W55Se0Qbab>4&jnylW|@Lm&EXv2 zZ0-OYCj)G1_2-}Wms92CKE~(n96u>M)^)n3rbcetr+h?W>Aes^25109fNc_BA(Jpl z(f8X9V7P}n^k9UW$a z*=%mTcu4HUizn-eK*`uxqNHT_P+~VOBZ5; zQBaIHBW-Qcq;z0W#jO)bg28wk<%2J)@%ZXE8W@1J22hL&yCTur0+qG2R5XIv)RjBW zo-9jiRScWoQ*ZC=R9Bz*sA}iTncfpsC*W2g^&a#@aosVmHx@fNeY`%t8m}RbU$6x& z88wK37XfzS;Gnp9Qz9yzzBxe4+@|t8Wiy#fP3+Mh_c5dk#FXDgW3+At!2JHnnT9Jj z2(ht9TawB$Y4s+-RasP2T->aLzE%tJjG#uGX7V`59lOF^p^)YgDfz?mAU3mb@BYx- z$mK?7TL*~o{XA{h8n!=QfAIYI*z%zllr*laG@$=nxqN+nv~i<#OflQu)>hkIt8-k{ zN4XlCM2ySAslUDYzqe11cO)v%zU+3Q&Q=96iqTe-^)Or#!0tR*PU@sII*&Yqw$a;L zeWv==xpTc$y;W5^2M3`&OPb;`i(a?e+xf+@WA#BG3kGZA^#!pwqdowuSL7Vv0)s(Y zJ{9eZxjSPjQ+c^2Es;nHzkTP;xBmIZc?@Owekzrc^7%Z!f5|#Ob?Mf#2kYyB*>*2i z#g$};z(SlNL1j^y7{qP|#UXV(uE{cJsH!}>E1W)-lHHuq8q6IXGcyqB!rIWt$i!5m z!DzMmbuyXEsMYcef#=^4-8vP{lHry{@EYpAeEISeiV`d$?d|QgJv}|OJwAq2}X8@Rluo$#wsdGMjKn8vX zr>d%}&sF0Pt)1ybBvS9~r5R>X?{@3;(dcv+hz%+QWvi?4OpRE*xzmf(gzgnmH%m16 zx~^1;&6TEv<^3iT;Z+!XA4U0NV!t3Fd)Lj=>G=Mor3LN%xl6Ymzj(e8nbo;XU8c0$ z5)y|hiz+i}fgl(Zi*Ktlm6hVOCaXdo%jQxDIi*~g3lcNV&s%k}8STRS%G~*pxjwtW z>^JCSw9L9>;7Qve_Q))LDeR4jmWIa0DdZ1CwnPg!NUvV4y^4g??)Lc*$8yI@HG*K> z@#Ev;<_<&?m{0(j!Wk%OZLQcyjQ%Z!jUFnD0K>O%D>|&M(!ESk5s-ZZT4(TCRaFHG z2xa!uwk`dZ-r;aaZ%ztD@zqtd$g(o!s}&GqTDe@7GUdCvOs2d@ zLN54SBC%imm>3b+&;A8*Oy{4RTnIOe5Mtj%j01})Ri0~-%Pnf~SBFA^%8cBS31-Af z^`5*mSI4rdQdY%AqtTc{hw9^N{e0fc%*^E4!?}rxiAFmDu>t?g;~9A_nZexNKC1{s zN?M^X5W_*ok9h1A9Rr{>&~vrt>Q&#?mZ^L{UB~M6ok^L|U?vthiZngzbOzw2343en zMhl3c=l3;^b{{Hi%;QM_D3UEL4+=o6x4O6L43Jg90E(N%keG(iBVN!tTE?3^SuX}L zc=2j;nX*h-jDzgdED>o)USe|vvD|bTeku`(3T0)3gFkysV#F;WvJZ4FTIOH4J3rhv z@iEa@q#Zptn(E3oNi6CEG%^8LS&;of3|6zB&DCXdLLo-ZR8%Mo4dIT+CLsCcXEH^Rw+GMib4hji{ z-I*Z8>?DB=nSzAY76QGmVe0Egj~=X#!9Y>7_JG}Kq|3$4#ivf30I*(Smz>HXfmobQ z+ZaZ#cOzrTbhxF{b*uID1*j|zodw0s>SnQAF3Fb)Q|@SvvWW#^Nded-a5&{)*>Ap= z*lV5aonP}EJk-;C!_fH~w;n&(I3%{qcA>pXG8r{aCMao=BmOtzAq#R_Hk)O4kL~WJ zSZ^oarL}5}XoK4DjLWsK24WK<4R)i|&v$_s9gFSm#$+;ntN#Htn2b=8m?NGOUaiP@m;&q( zx?J8|1Y$eY=Xy`Us??RFEHs#ke2W9H28V+NFC=Upc+DUb6fcPPL?TU|U7Vh# zqr%*tSllcSWW*#8C^O2zci;Iti5+&bBX1mikLDo;46h)E-+K1Vh7&%Ivdn^*NG<`e zkXlk`+S}f&gu!GA5*v&%mD1zyc~*@PHFF^AKyOh)Tu zE|)B-5!KlW@pu`cUc4Zox>$EKO5+HHU74U*DG*dD#p2-LpmOlHN8dOCiM>wjur80j z$H$~IJT!5h5KGu~1JP(ImCJMUT@vJp42a1w=x^f&WCU_cT9Zdns-o^)VN}M0vSyH7 zQJF3mfBxS6k%^J>Qy^wQRZho*rKP#(&G~7Dot};^Y58U?Ip}zJz+3Zv5d2GIJq zd>}@#ZoStri(;E~Vq`EFIy#)QvrcoHk<3K_IOWR9&`@9Z#i=jXmuK6P9UxYr2+X2n z?R3AyQjAINOmFW&F`{d;#Ui&!Y>eI;gT(QHeFHj(*MuY~s$X3d09cmFcv*8ze6TS1??>PHA&b4%{NDyLzqX=bXyTI_kDo0^{7W<`%Pf@ewz4{&3jpqG7kCEX8ms6eYriZwtT>0&1U*&Dx!2m)RUKCS`3A z=d8itR8&+{C}127D-SX0e|_!iF#t1mC;}TB$iF2Oili@O!Sq{Ib*8F!ha=AHS>*fs zSxpn;^}1yPNmOMI-3el}PgAFF(RbS;6r^OttmaWn>VGn z?oL<`%_q^z>L<)5@7}#nh;@${UD_F*EEdbv)ro9%$J2$b@48emtvCqoohC9*8}M#!!qL>T)xsrTKhLQ_lE&Jw5@Vjp-T87oBBgP+JQz|p6!22JS;k6=IHyiPVpS)4w=-h24h=<_u;z)y95TL+ z*6E0G`>o!64Nkp81BKz)P?1R_;7J%?OuTw~brsNX{_S;Oe~;{qqaXCmeR6{kYXh*| z)GjxF8pt3Gi&|_^Lt{JNzT5_}{r!ElB#-ao#mIP4DdfP{@d+^=Z{g4PKy0F~uf>_< zCo!9VS3D>eot89xM~G?7W*3OLz)a?5^LZ|lHT5$lpPuDnn#TIi-P!4owi%5&zHVSZ znglkU*@?to0)@dSS019(Vvu{@?q*Z8*>k8dPk<0(uZ2;;u>&<0EZlrnL*>}m zP15s`E%>ql9pA`zby7AE!`zbfg8AXMNEV%6Tbl{qK7Rc8D$d|MkYY)MMQ_vjz&u<5JUd`?zAN&kYfhh*})2S8{2?=>cT#i-{rc!v2nZ*qIDh- zjTXc{yY~?3VyJJ}ZglZF-Ar9PI4Fk3B>P>Z*m8tB%r3Y@7zxg;$FQ5G7*lx@R=S`^ zku;|7gvxN>ZaqCZDN+f~-(k0dOUv-^%1YndrAq`@#K-1Y1Y?jySg|Y2X4;e~#*}oX z`rNAnwWTSKdhg1yLLmlbG%qPS7@w9I3z>5G|nb(ZpoHu1N&)YN8bMZAOG<9*>jzCa%m~L*tM%t<+H-*?(uqYrYlI6N!38MxwqfX zU>(4obQ3wiDUFa9s>}eHff%at<0~U07wyTXNiUPOG#8h}3piM$ppDs)V9i>xd8B=K zki3nOm?#a1k@-SScDjs;%XnjY(y%*047<)+2dy)7R1Ej6^bMW=RdFm$UB^|zX#|QHvXqhWw0!*Lml~x;EzgnOBe^4+ z?MxcYR>S-P1hz2zaHxBjn7&o^%U=tvu-uOX{Uv`6lLPFIqq(k zO&p91DmmDtynI<*O!-Xxu(jPNu4-Ulfb@KX{tir*Guow-(AfD;E`eCb)2C0pHjCgx zb+fW;us~(M%SEUC2%8?VM$^taw#eA4v0)ymua`(fHc>e@o23{kZ=$?Rvf<d zPa`|5OfY-@cWp~cLaaR46p^+nniB=ZK@dB@TP^3#$Wts9Jy=ire0ungZUFPPSqoYi zx=H8%>^_#OBNN?;r%7L(rXc2UJ}g7JmWWQL+_V|N-D9_hfq0S0MX9VPEtjMvO-)TU z$!+vZHpftGE|>Fpqbax7>jg0#mYw);d4?Z8giRtZt~+TrQ;^h^M)_Jh%G~4hC?XEG zEm$b~(}}98>Z)QC@sP{WG7LJu73RxN(n$lFSyPSJB2VYpab8D>Vt9BB9jx#2 z)W}q$)f0a2eN^OY0{dxX@XU9P{_4G@X<uwm}>)-^R{ zb5T^Q6U?wVYjgPa<$D>dE_%;!EQLA{}d*-jIgyEu|y@B488-1{_01t@a3Lt*E1+Qg4NWWl}nGT5GG*dT-agp1Et*y6aBY zbsdrtDTX5Ab%c_ri@AtEx{cHf_{|~c&j6+{5D<#JYSqmv< zw8df?mBwxvg@~=dH4Afd9lU9-vNA|%bF(!RqI5zJa_rK#*H8WO9a|=o(Y(FSu3_{r z6$j}%)opD-7$>C6noc_$^#El-2&efM2Ud+mlu9Pe+;Tdei1V2Mvko-zYWn}o*x17N zOVs3KUq#S5(pi!hvQp-&YYPThthhT${kDvzAI?9nGFb)+_YGE{E<;aY*7CVYH@--n zjt4XGf!<_W33RpkruxpDVM?Vto<4i}^yF(VKeJo#dZ;2F4cU|I+t)E8!_w?HM@I)N zNIdK-Od5?nI_g+1l7>nEhWy71Gz*<+0OR(g2~EsF8lkU#mP`?qft|svcbsb9Jz4ivSB`zI+ui6 zw!@~HXRNTP>fsiY70Byi->smoBVp)H!r!Z*eeOEK8ou|zJ&m`2{OxgdERfGG!xmeQ zx4P@9-DKFXk6^5?a}T+!Bj1sPfkNG3M3%F&XD4TuP^Vr6uie`pU67A1O$D>fFYHJc z@&nN*N-wpOsnVz$@2&2sUkMw^4PX}TsQvYE*Rdf&SUiNs8t?x4=GjY&*AKi=*HLvu zr6##lO)-?t*n1IU&R`C*8tG{n^BD>ThjTDsLtFvcJ{Hr&v6Lb%w)TgZpn&dxa(U41NA(NgDj``V9nSkzUSH{ZVdo3}N4s{;AL z{d8p~6&-TBK&-l@<)2Bb0oTMZ#DtK-G_k!FQW=|=Y zQIi3F(KhtX zUY4}v7Y>A)8cpn91_$pO5^iL|mWFH1sVtJdu^6z}Y%7DtY)O5|yL;#@xg2UJ{b@eb z9;!w{62K}Z5XgcRNYxUA8g{#K(Yj5*XVj{3HNn|Sv zwQrdLbqGX<#~{wbz7VQO$XxsG-(tmxXXfzfrKP3Qw6Dptr(gZWLwIcfvn`u8f!XF= z#i+#?LG|WULrv5-?ya(|{FOIhnLT^SJGIB%P*`QK6sl-Wc3)mNv3Lt*zO1(D2r@EV zU!$V)$58K_xpyHkE4KvlpS78GIkgz6>OV`bsn8x=v|x zzUGD&8KivGTDQB?@=r12@4FG#mzGGfCFx!MI*6Ph zD!8>H5x1?e3;)l*+UJc~R_ntBdC6U@repL0zjk5!gGvPY{dwZJ>{+&K`tKmdkxi zTE4FixO`(k=qjt-t8N0ZYbOM;B?Yn9cR&0-ZamR8?*K4R8!Rf!DPyzUtIC%+Q2Eo2 zN@j8MaWiu+iv{vbkue(j`mW*~YtzL8HcOPc^lDTf#&gT$5Xlxg6AVf1s{tcr9#{oc zBZE3w=G^n$DsoRPB5dvP<2;VcUOrz-8wzX7P;fudcmC4F>viV2Qonzt&}pq}sc2~E zX$bnjArY=pzd!z)H~VA$mC=GYj2Ob`PwKB&yI zuW#D+BM^H`m~Gk_8T|cVB=T9&0%Cp{Vl57It;+avc^<@CU9H+=(rRGy0Od!YlBTJqNuSXdje)F#DY4@NTG)z37(YY0O|*oS6+9*N^_5It z9c9O%6|&Uxv9UWe#4Zl)BDjC*;^I+V>s-K}@n_#TKIFsU!ybv1_rKu{a0UK}mL|A=jB~W#N(R?o1pri!h^YZlEf!_Vj#M=1RwkM!zbyrsl_**IpCV z>rGL=zbh9ne;1NzyJtd;&+ydSjQkS8s*p!jEJc{7q{L=qerK% z`mFK#Ox1Vy@};dhUvu!oik3DIbInykU~3xty)}_YAfNMecT0Am^BjY&4UdJ$Gi)Fs zx7F3LvX{U9D_>tj1`){ke0BGa9&0?a3w~(~3=T$$mZHrREf!adD*;rEa3S7N$Qj~^ z?(T9!TuZdixTs1-54<>SLuF~lV9})4S7~Zgn%H_-n)HIuz$gvBNO?C!{r7AZf03{u zz=XpjS+*oCbY6y5&~b4FB;8ogLTc`d;V zz^VkYBH2fYn<7VSl%@1Cera1B2$NxAN$c!<>-aILORJks24G^TrO~tN>|F8Va>@*V znATOsP$^3hGyWEf8t!Uh24^FfY@cb@)hTgI(o5L2+uh8#)$xwsHu{y4Vv=JLkGCj^ zte)nq+K^)9KL=FPgtG1W1xufDCYIX z#MP=XHJKV?dzm#&F$FQ3pWYaS?(y{i%!bsQuN25iL>@D1L$c%Hr90FkMM*SNG^e_% zFyyZJorO88&OOA&Bsi?qPO}uXjH~ZxWZpLcFA<+jI**tVy44(7TZAQrJnkP-uyfs!1Gw!vfQUdBTPQ5SN-Xxz{{Ir-mi-NG(0 z)ataENG!V->aS79qD^*{ou;OWsk{+t95N<|+1NAjDDNPdBeFzHC zP|+~4f8ug;qKF~)H)>11fW6m6QR2|9)1{`TJ%PJxcNzjV`nBytRGS_7s6*^#yl4nI@S7d;ESS~E*3d_sg;;@aU z%~nx+&@+1{Fa7UZw^&q{={i7!G2Skrd9TSH^S)=-tZ4ua$D2(s5{E-E8i^=V#UPs) z=N@dyl|WW2+YyE7x$3Yv9@xKtk6W9L#`@p9t0}TdXO|6_crs{Ly6I`>-~Mf_(G&qN zK}0U}Wl%aRMftePat9(`A#JqXvzVGB8;Ua5 z6bzoMLkhFJe3@;`yvwblD!=&R#sb>KRNv7G?1ZbVT zAdr3Z>h>Q!?s)Q^fA5ZNO8dCHywee$+UXJVi_DA;E*AY{V_b6 zE+Nl4MdcllBa0xLw}po0lt*u3wnqlH8Tm_u#mbAIWuWcw|4sFF%#wACB(yFXzgvbQ zcz&I@C*3{>t*&}L*?r|JYVzjnERxug0@&GCwmf#kwsC5cE42(t<>;Fj#xpU7>d0Wu zlXH0D>GE<9fF18hhUVG)31lZOee%a!Z-}wP-BBs9+pa|v^z{g@T{>+%uJ%@7NqgS1tL=0EPfavQ*IP?JnYAV#9Sx}CoN*voSB zu54-1NEB^HsHTjfW;k=X5`3`b<>dxLlHQRzr>siFcX8p;C%1YSEqh#Z;XqVMjzfl6 zjjBqm@v6P5wKbJp-=wdK=?N22rEhA=U>mg{kg&TH$O_@C?D*py39U{s&X3_0Zq|-C z@2n{5N0%xl(Gx)>d7cnAXA(lX40d9M7*?c29m zb|JccB$yGCq}bz+ZBM+gBgZIS(fc_Nb97~~^DBrMB)tfw zC~<}gVoc+0W7?8?7B&QgUHZS8@&q}|Za{_^O+t@IX%hQB3`0l^Iz~Qh$ft?IC?iE` z8dIO_(aKyYhb5!e^!N9RiM2TBNa)7I?b%aT=bA&%Ff0Dj_OWJYel;YYO)@%;H< z`72-DCLpg%AN^L63_^DHQG6fi29xmSr#40~!r3fPtx%Xye`r9SKI6z`Wzk#CSefKT z49FzKD9Yb)({}VI#;%@*>dI9cd_;0ArqVRZuEeX9RTCYbjB8{MdS`Ep=HGNg8kupm zw4@ZKS13S^?-!OmadXO=YEM~ZIj(gTEK#J^evv4fj1G?{IYz}D>Zxu{7An)N+TpRW zU<)1vncL}Z@+%~ZVZ@7Oj`MEc{vWq*|AnH&s0&`d<@1Nx>yv=>($1YP9(i%|2H3|o z2u3#Nq%8eLVqkE966E-V7YNu`uzieA}JN4 ziK(hIdo?f(*m=#raPAVjOWLXbriz(NHBGoy3A|}251#sP&Y)~xlJoQJ!mns&m|MbG zW24;r&=-#wjibv+ox24E?egVhHf#t-da{&869&AUPi*YD z+=1$6VGj$(a7G-tO4*Zv7@Z$(0nvHT#MX=Q(?4GC+s`9LPd3lRvqoX|>NO32P-SA0 zECaSRuUFmz-cF4QLe{9(n^yR1$r0j8F7GZc@9xH%15=#m>3Tem|AeLPxLNa!zA#9wATlKQIxbXN>V;w{l%7tt8wcS&;R7v zAO=3isF8uiZhCR^mW?FYGrLxRY>*Mqh!4q=!=1%jnN{wRlEk?Oh)LfUztW;y*jBV) zglj!MzyoVR^dSTw6@|#QMEPf?Oz0U$L2RiqjhPIHb>&72GN=L=A!ZW@suJ2n7KKs} z1AU<;_9*@y*_rDSS!dv{?;J;#?LDz@;+qqk@$`~tHy8N0@x{ki*6~_Tu~|tlit?ii zuRp}awr$=0%1=%mm%!pi2wq8XG^L~B;+E0Y@uNMB$sL&bxYHzu?udp zWa8hhslB`j!pWk}0guCxUSf?+cQVksDUUaap%E{{RWC` zb$exO-pNF|H!?>qUcUwwJ-`e)!1N5)M zAU2DiLV}c%9quD6e1!V1FlfcK;{tuzOB%+4JnrUy>`X|B!_7^b4zjE+l@3 zqoguV`!ewU$K&NmGHj-K=3~jQiG}m06~qosad!)1tY)h_-d!o8_rqOP(uBMk=1KsQ z2TyL{X=1#eCd!LpHQQBKc2JR(@=_|rE*4gKC{xp&rv80jetGiClPACA%kbD{I56YW z)O5raEoMivp3Y7@bQM$L*&)r)Cx{Ci7aUf=`(9AP9<-ihukYUacd}>G%U7qS{<*E^ z*`GW|HE#P459B);F2%2ejtF3MeFNP_m@A~`^27ur=qlDA6M8#jI-eWHBLOn*Y42EWbib}Oi_yOBF9%v-)G$-RVg{vjueRSax)Kcx^({F;#m+w(m8u^@5|V)<1jQT8DqCi%q;9;8e~%kQ^qyyGO9+KZm3XGOxsl0zZ%BQQ(SALpyjovW6r zbb(khVe5bY5AUD+r(c~sIX*rvikn@pu~X>`nN8@zj*i5!IuP4GC5TNDa_w{Q@WRB5 zh->&P(Ds!M_S=7FVh@+)Yd?MdX}P6;9Kn0ZVB`!y_dOo@Ly7r&hNCx})55iTd(R7E zpMHMv`WdOpDbdJ!w4=6&%-s*f3vtCVM$_|!nb`ph#27N!S_Z@pY>%VPu8|I_&WDM;x9{-bg9p!Fya;B( zWf!hp{_{0TfQy)=_2G!2mmAt=Li1c2 z9zQ;RF4-Z7nG@3%FV~Y4CFjg+ztw6~4|#tP7-KcG{!TMwmi_W!t}g{KVv)RGxOr{n z8qwRkFbjDVyg=-S#C_~4k1=>I_pr8xzZPUh<{U#VJl&VsTX3FmnA7j`2PYB7CNK7K z$-Q53BevDKN^DY-`n?)Tkso%12UQK$0<}nsDm{H@3_R8w_n`|>nCOHvCMnXKv!4iw31_RSmsL7jPD`TN8U3j z{kiuVSHA}jvp`Z3?9<MfDhOC3leq8#FyoD^QhV(h6;xD{fsEnx z$&KdI9>cN_S2KDASFBd?Z+FxpDe7F)fsC3N9OI@?G8swrn6orImZk_@441K0OB)Mv z6V=x%e|x=C`xh$sj!Gwv_Hn|>G5-n90UqSoqm)Fbzv9r`{QS?Jd+BBQc2fVi^XU!c z-j>a~!Vcz1^!Ma6M_l428NOW6!1*>Qb><-5Ln(zb*Lf-hE$&}}ZLa#FF`9u#RME3y zbZNUl#+sHyQTJ;yc2iT8DQdi*9|#xZ?d2nt(waG@P9>%jR?-;Y;IZbe3=eFh*SjVu zHqLBSRi$RQg2v*e&EYXl;0+#bd83za1vE89iizmcK*v9EwU--KAfo`3HYT_5zj)Lt z=%+vbhi8AxY-4mHpkkct${~_%Yz^r_i%^Sy zotip|2HVqvSHj&PA}loZjz+z9NAq5d*DmP+5#tJy%CtwDK&&a^SuJ`+@|Bf!73(oe{_mE-s!qb)b(GFQRo%u;o~mEhjEAyIKQGm{zoYCq6b1 z!!jBavjHIg+)iv9sVzE zXa5xSnPu@!T+FN_YA_j%N$p!rrNM^gCEcRf&0EvZj6eg8G&C=YAfnh!BS9picIzZj z#xQYIbmAmg7u?>hjmhdx)^W?QRY_)x%2q18G`)b(3jfJ|&NtTnq?MJ~JPoaFS*0HD zJK0^CKHH}Txr?uI_XqqwC}aJ9h7;TjdBMG7Y5-&gz>O#q;G`( zAos3q+wGGXX>>jqqDg@DbZk0d@8D~0i6XAmu#S^O21Eu;&;UMgm%y!ylgM{0-(MACNhw>4o2L9ek1Y}j+sqo{ z?m^ef-_9^y>lVF&7l<9$kJpt7{+&Jbw&J-tAy%G7M!{f*VE#k&`cu_{I8EnTb}iKm z8PT=Z#S{m8|DYKKKZ!s5l@vpi`d&WbucFFNYYgA@hGP!VLNRtw#^e zl~2afI3e`*%9rQNncXupi4446mSp?q!+Zhh^0gWeW5+q%jkutDhgarcl(84y+p`nf zl{U7blkZ-jsScI>$DLQ6d9pU=HCuGJy{H_KN!I-h8#k{{NnZPt)%5ybs`J&Et^J;` z+3G|**oFrM%L`;+W^jdCJo<##OWGyxj$`4v%<6+#AeNZsqLIB(`AK%_l#@|eAxT*W z%?Iv*FB@z!POA|n&)kdxm$zQaF|JVB)u>z3oRUB#qP+4sa=zOiXjDZk_be3(R6BQ zJ7k6N8#Bpt6X{uinHpmzdj>*JvQo2^M;t5~y091siSmbe8y0qaj2~P`>^>9wIU^3F zrB-k6w0QrE`KioS5NqnM(`2lZy~qn*QWg|A4a8XcyD^nQm=_2fbM_p@TJ!3~i=U3& zezM>qY26wY1sN-4&2y{n_M}*?R%gI!WeAA~$5TO?2SMW{1-fBZ5Yx9&i4kC=Ec)SA zjTl4klPJj4sYo18beg2}$*auEP*S3|kPwQ4*RqA>9}X$J*dpahC;ImfN1L%0BM~_s zaZ2hHUa?-lUsyOlKQ(2jvprmAT3cMY(3S6}S7ViZjLSj%zuXUieGE>tSasKX;KDmh zGhY3aw0!a=_rHs|uGKetHd}*1A`HX=$J&T`^nqGC@oi#G@thEY(D30b!)C{r-=@H@ z`Iur~Y0t1vi?($Ol&ZIij68${8ET`RXg|1j<%k=^)I6FMVxVM$Nq|gRmHV0)5sig-k(i3NK`OOGBisc}2Irp@ zjggZav+N~HQ`kz%k%xuY6<-JhP@2Lvw>WEX#4!+K`i);Xr1haMzqQfGr%1v&RnXku14%+N))B4 zKdlmj({njJYY8rBB{I&nl!dv&zFi(IIv8AUOw7BmxoD|3 z*THoBKIhx`M&GzPHn#bh#~%itH3s2We;Ra2>pc-L6Z11Eq(y)U7ZCHdY3~$LVl>&? z0v0TXQ(q=FhS?Msx)C>|ovHHuY%ikp$LY&BQ!@J23h&4Fxv4DlVrIk)UiCV@UXEIp zo9#0(4hTbyy~Z{PV!dZ$u~h9FwXj%Dx@MwfQ6iaHx|7VK%gI?mi2aHo-2*NvjRInf zD3h$JtZMw+?Mq{KA5Z;B>tA|BHEZ%Z4cRCC30vhRkB9MQ4TUrK!b?#`=85-Zm}X2U z2r_L3hviO(VUsH-b8db+fR&rl=(VQuLz=z_FcVEi7V;So126}pF@nhNhx5%g^+pE$ z#qw5O%$l6;G<9mW4afjYVU~zV3W^B}LSXI@#2()lCP$LoMr!^+x|7Y$fS;*Xh z2`fD6uX;K?zK?N9@}Oq#7nx#`t_-p3_N+qWMg`J{hk{#mV82uwl?bSa#X z)Jt4eOBcxi{%s_sk1D^;79GTzF2o0Sym>x>JtD59z1&qQ!obboF&tb=9e(*DDc?W| zSST|9Po9qT<_|wZg$`o)hW6E%wW##s@f>qi)#%rsULAWpV#!-Wv$Qtd?b$a^S0GV- zdCR7hwW|@`tzqiX%1}SdA9Mz;D887}_8O#b%+dmuJb`KAVbWJds8t26g-z$r*H+7M zS&eEImp~PCt5ahnWD)IsRs5v~kG7%{&X2d|FS>`LUtNpl6kd+X$uH#3GOBG-rYfH*=tC3wy6?-B?7v)9R6%2~?jl}e}srIL{ zLRKmxwFB@@-H)8NpqP=<>h=>AWMPqX7LAP+c@lSapnf7(k(%(0e|_`feTT(?D$x@4 zk3?;xmX&^&&mVWhA1uTe18kJl{Vnif$xpa5&5j4dQO4v3bLwn4vVfD}R1JqqyhPX; zZRS4JV@L?zd9%#s3^>yZFSEJC2QG7)Q}xZ*-Mf!1VX2~Cz}XCvI8s@}Vo%SmY~Qpl zX%$XBkUdrEw1MBH!J%gpfVEov~+0?9zy_MxDbG zXkYDUhIJO#J>`St`;yh-bs4{GiK2iSB+~}T(7{1RyIAb(TcWXB7%hRA_$)0CDb&%U zaZ3ouqTHE^xm2N&oxlgFk(D4OtASsXIa^%mIXSXuv4lcw!d2hg9b-MVbDg|tEB+^o z@gwawi86Y597{I*^yyDj&u!F5xC_WQCIzotq2gDViOGbRx;w~T!3ykTpDD!lw56A7 zNK1$%I?9n*7&_0f2^d zO$m{G_ucok1=b^Z$c*8?0T4?cQNfBAkbDV2%wT@>K=@RVm3TgN2h1*;dLk^Jc9x!R zzP?M4J@zb~^i1-ro3?D2UCeq2Wawe?m~Gkk^oTvXwb)Y!Vqq)wh1EDEUGYL+6lEl4 zn*jU1i^vBrKZo1Adnm`|CAP~3u|zqiN>-|5iJ(``LT7^Af0iR(5>Xil5MtK`EFi`~ z2`DVcb@Edo`>sYvAH>e^4R&21X4;;0bdjiJsHQ-?C1JIWNiFE4N27Ww1(wCHJa#0Bk@2Kw*R0>T^&wOSmld7P+CZ`U)WrWn#N)A&k1iL5 zf4~W10bN6QLCgpOM50$84~6WWK-s4oJ6E8ymB*m+FjRC{C&4B-HjSFmE&sOGj(OuFLxa95hO)s?z+A+pV4souT&>@C6!#EjMT$F9ak z@;i@d%s5&ZoDjn4eIQ2fD;-C#FwIyH8ySfjh+zOi^CHqmWl^lDO+YIS_vq<5WA|>q z^a!yhOZt=7Z`yMD#D>jJ-?U!)8vZ;O^z@UIp5Hw>Dg^}<*|&pJlMYKp?dp|rHQ~mi zX>9)Xm4nsg_UTCp6HV&Cq5_coDUJ46hCQ)RZqML%lu(E+W|ovvnV+R1e|9WX2joL_oVJW2L!u*T6taj!jAi=0d7!>bc~S z>mG$aVK!3m(MO+s`@zMlJiN(@L*X!3`PopI*&H7q1+lSvFD4T@KMdNsHH6qx*A_2C zBA$pf;;IVJ=XY^Vg1m6*_Ubq!*Ap+!d^!Ofrjy7b_Ppc$B{=#Y%Hu&eAQtOX*V^EH z%_o@v%!JcBzE;TI=k+)Nq@q+`m=VSY{2_H1-SJ(HED!@KA!fWab`A}7(eZ9ydB9YZ z`*J~{4wbNdRXz7sB%Xf|#LOC*Fz@^5vtNQ3Po$U_XNBvcb#-CZczAqt^wOPsU%rr% z^nZ$%Rosg_`;t5+>mr_X5DSK#Am;UIXq|2Gdw}@V28KE(8NLA^7M^+oX<}X;5W!*z z*6~!{Ivydmpi7YjToi>E4wR*s^S5H3Gypp-Ti6I7_Etx;>Fh1(5+&yZF)Cmd-J+Ev zajMMR;V6I!F+G|Cz__^dS#5O83d80odnJfny!h)&LJZIU@o=yXxW*^0a}vMvU)KXq zu?0T;-7bJ(r(<@8Y4~tk>GU>$8}HB;vBk}j&P!#CJF2Hx29X7(&S!5&%b6;)h%DZS zFkY;3lMu_xD955@Nr>oznkM3h|0%(k50aRCix@()#)V&p>*!_yIq!s|wi@qgVP1iOjx>*sFYxc%is z@HM&@KgwFW+2^x*Jb+fHaxCD~X$iop!m2QIu4K;?TCmp+g&lx7o4%-?qmh`Ho}NLH zg6_gD%yf~2dY~drxH0oEH#fJ`i~}%$otkG7>F74}weT~D`9h8d?yN(dw>}x7$*(Z$ z>Vm-zoxXW;+LGo z=g*(-C@)V;>$2Z)Y%o!`PUdAG1&Eob$bc{Vgy|3_cLJ<7pAg%Hf_qmkB?An-I5-Gm z&H34#y;NY0y|+FAu_2DeX*bcaH#_dT-47qe>5Zkkc!Ca21I1HZj?2TKpQiy~Rybfd z6#mn1fNbpR2~e82!p+WjAUMH8Ea49l$#6tA9XiZnqnv#yFU43=##UzDmv5t$OzxwBW|i&-DnP~ zofwTOz)mT^Xcv$R9#35u#U2iYAADd;droKtgP?T<=#B+B=DxE*8Y^VvFKP$I)Ao8U+`1g?O!}s)@eX91zX$1g9B2hb@ha1UMv^c9Vh+G^IaSW-eS4_C9MDU<}c7^P5Nw3|#Eh|#4tax+qa?l}AOiM`Ek z`Z1wFR+Pr8Ed+y|;>G19Xz>@d!u{XxmA?^U_a=ZU804WjJUJ&g(XaH3eC*)?O+T#J zS)B@M7tdB{-9y52(d(m z05d7l@R2$GKeU~1cT`oD#wV!EGzvjOqh@%Yq>`#iQn8|`+=37)RG3^+6p#Q(-2@Vo zl!>ARQV^0RX{9v{2tA_RqoOS`&Vr-`betJy>GT>V(CwQJNEOr{C~m&V{GJQra`n8^ z`^sVy&s5b-)Im*qvbXoM z*JLpEW9hgDU^nC3x#O3{5G2!>%y#|!p+yKv=(JLzAI{f82u4`a!Ppy8XAuc0^;tMu zX=A*O$?!5&WG`L6(ceEecLV?WB}S9Vp74pH(^WeW%f+(|0uj(81K1PI*%n^LJi-=5 zJf(iUhkieQZ=Cjwm~Og}4H&EelYujyoEeMe!u9+hGAj>x@`a#xgDlo%F<5GUwH?v+ru?_8~7xlsE5=j!H{&l)6<*yKZ*t1^uPVKg7DOEasNM z08Zwpy;yb*L!P$*^1PJoI>dM)Rp5`inF4D)hlwzK?}kazeh-PCPo(A$3Pr(a29NP_ zO}_joiW|I1s$L8+0Y7=N_p>h{DRx${-|vLG{7#g8pBgFsWwZVaLS-&*s{XNs&i%l~ zR?;EO8{Q<}xX{`vzC*pT8Fr>$KCUAVWNMYw3iK|wr#7+=BX{slUOsAPs|YpRvH7rd2YBJ-AK#Z?F;e>8Iyvg ze-J@WFWkv4u!;*@TCDaKY#Z{bWG$Cc3SbJthvn;emw3*x>Hb_PHtKu+NVIDgH>>Z?B7qW;g^#mD96U7K0bzo9o^d z%D&rdlyT#X@h?TIom@~baQ6gi4snV4iyEqopCa zmzYjI%>J|QCYfJbO#~&$Y=)>D^w%_1YpnuFx;P95yyQVd@9G4f9;=!N(Yj`79XcF5 zOyZKm-*mY$%xG>$_*ddt6OGhdw~U;(!MHB0lhI0aNjJ-;8`Fu;SG9*guECZ=<3iej zajP6NjwNxiz|JNMJ&DS+=H0t6W<0ldxAGzw|o~@t_uW@GnE# zJL$aTl0X{2b9l%-;d}EpD;jhhjCR}P44L1bZSBWeS$F?jE8YqjBkKwUW7i>ZtU96; z7z`c>Enu_8{N^{;Gf_+ySLOsB!++CGbUWx6qJ9ldwxBMC#d5@r&AfhFJ9}ujRx{aM zkfY{0M`A&M3k^jSlLtZ@`lWoJp!^Gxbx0tzk+&Rv9uuH&zY~wBV9ILQxKC*27E5w0 zo$RnO(qaw3(&pHLJ}vqs+ZVZs}nPCf62y(T&X%fXyIGP>mtk(WAady(@ z!DK!dJ29B8yq`+B5D#^EQiXIP1ym8@?0aGQ7`fnl{4Th&wt?wstu(2eHCbg6F|W7= z9mdqYNYrv!T~a@w_gck=`s}l-psEQ;*1kdWUk8orYc%mkoXv~2z=r|il-C)lEM2l> zLCbxQ3YjYiNC$!tmFBvu z;|DPqlg_E3X^L5sjE28ClVUdXWqcScGcB$ZP%>#ntb4#8^lLGd+}~R~@{8O4L4Tv! z*cgElnqk)@> z*NxBXQTbeRt{+4%B<6y}IR+c0)JNAF#zdqfXz$$B|0vuR_#8n0vH47t*dQ#^9##!t z&q82?lO(GdVcVi9o9n5$$v0tON<*Z9gB4|Shbh>SBdu z=8h{bXuC+gPU@Nj2L&kt3{}71sy5W#PH)i4piQY;BH~BL9tu^6!4PS)3+a2M@XRbs zOfFef`%!H;Wfhk!dwA*N?4(ISn=4XU^joFoumgz04n7f=<*=jv8uC_1RzMa59qqUn z3vaiNMFxY3d$1;Iqn<^NRuyB)V>p~C zM+iaYykKClc{DH-k z_(yjRXb&Budd2(Rn;3Xtu1KFRC0PuEMBRy`crE1?@&J)SZ0vb*beYSMAlLQqd zg#gw`54-jFSe~DG0C%(_had;J1&mBf5QHU5JLMF2JT?jL!4ccQQBog%^uS`G7<1l} ztZ?l}4U%t`G5_<|)x4~y;48G5FlT)H6lPm&h}cC0`7 z(KFOwF$elhY{CG~EP5>~gImX+-!uFiK32E>YwRUuSNse>G{_hcVMB=BlgM@2X_&zq z%Ve=KEGG;^sz-Rn6w>|>@Z7%>VSm}ROa1ESdfFfa%+gtoZiXqgWw1ntOJTb z8n*ggI*-AVDabfa_SVDq%JYb7|C6u8xlZ{Q+qaI!hBtW3Pu*-*^t&U`Zfo`5LVB{g zl%_9u(x(=}$(BC79GZ!a$5&{)smctp#99^$WU=PTcecbft5Rfo|KjfL!vNFAp@WYV z<_2z!?j%JI;b*UiJEbuKEui0Du<0|W3lXe|UToKcb!oWweAR*og(ttKt(k+6l0RhXPPbG}z=4_J)#w&C#| z2R4UdB-^z%LXTc8ueh7c#y&p+9#~Aot)1LZ7@c&%tB>4im>k%oLSTw@g`u*xcwe<>ax;Sk;~q={311^7 zBQD)~62hCyi?}-nKb>rpkj27;CPzF+d>N;E4S&`wyLEHt&YhPYAXNs$4>F*+0cA^E z-JeLpxEBs%Wh~YUv*c@NT(S^&nW5J&SwQ_t zfg%3XB=$H7>#9uGBwq$E~K}-JL^% zs}41k^@&E-2PC@SNmzwkcN9dAgwe}oG{6^3g{r~ZIR;A9C+%G)>XTBObL=I*3)fVD zGoB)Q?F7%yjCT{&1r8Y=E7v}T|_yGo&~b#rYJ39TWQM3{ zgrV+)#aMWI{)J&Iw$1O3z+$bMOgOnFEanp{I(nYdI{3}5Y__`sM00>hv0ei$u*A^KeeLhCwW-W6OS)n z_4GqU5B~t>&Re%&xKdQ*0cQ^gtSW7PUQ6VCZP*|x)tO;mx1kK5pxhs5Gu<#RiX%Ni zd1dEvQYY8aL#w;N-W>?5eslI zSG??d_gMPSijp;}@Qxqfu_$_I&8ij4eg@yv4=fh6a~r~5JB6>&N$1dv3Ts&h`}aFMT8=I# zVkZ+@LRFGU?Nn{dz$NOx4Vgr>Xn?=Bm{JbpuTOy6|M4+dwCvHJ{gu3Z(aJWL2RSecZYbvi-x*Hw>Ts6;RK0f!j539m310kZH=IS_zK2MF`)tjc)9Gk3Xx{ zg;3V(w;>#D;BLH4^=rAi z0n^XSTsZ&vwKEFIqi-YLsjkIklGv;5@2zEE9$UNKl73{G)_x13AFx>X{n5Ywp-(+5 z<^CnO$d5JI|KD6)`VGQhS1{q-r8BPq&qwR@*se(1BE#OV8?#bII0SeISX-VGS*u`Xcm_yciZOU;j zECvkr5(ffbaMFYk}@2`!6`5PiU(Gb{tI`O6<5#WBJ418w*}N=V3iEv0J{DVSnVIa3}0h& zXq{`G9=r)px5gkdrX-cc9h<-+$!ac6)M<5BntiQVicgkN2WcZ%Jcr=yISpf$;xdVc zamZscRt{pJs(G02&l zsel0af4V-PQ5(IA7C7Om6S#X%5Q{D1VQLx8z^N9QCvWB~-TukiBWNweLXHaaDHMN( z-#idnmj;c_h6r+Rbo`KBhdvx;Z}C)q@WBYI*jbYTR`kcLJC9|}>iL|;E_vSK1&?t2 zkPqx~>VK8}7pC8s=Dgup3=DxFQ96PYaE7J;WAHVL#jahsdKS8kbE2m@N>u(A8ieSw zgs8G544*_++V1{^ybK3(@l^;?UM%8aXJpVPQ7*}W20} zCl_SENW_RK-85B?MV+yQ(c3!$onn9MTzOXZa`%mm-4@G#+A4@}3MV`bBK<_?3F?YG z5s6&G%%xfEG5v-P1)hRZZYG;4ong3HEH*of$b9(9x3ZT=N)Eq$^dJA_5686`)f}sH z5dM{^7K`r+gJ6pbn@zBpxD+}zYVntuozhE=&5s;$H$ph zR^_99Vs6Z2kkL3chRLwAs;=$?7nCp3PpgM4mQ~w--ZujAlG=$XVUwWIvDhwZ_pnVh z%FmgmKTb|hUm_gkUi`wDzW*1@g?9~2Pu@4!Sa`?rQ}8TdF_GKB&hRUMMw>TdF@YT_ zyS(hz1&K*yp7;jkD9r|o7kBIVk=2n%O+?x=jLY(dWPcFa7 zbLqeqBN~bK-8SQ5oS9*Vi(Y>91)j=z#iXy#WL8##hiR=|I*i(U$b}V#FP{j9AqDC6 zIE2<`zWCycGd(k7W3$skmuB%TnMwx>@}W2J!{w|%@#;qw(O_)ZI8SHah>rnl3#e#{%=Bq( ze#SyOHtu=*xBqii8cCs8hu1%tVF*&8^1|e(gh-J1!rbY2mZsqZrPf8p!reG4;0nWL zwI*DTpJ6TT$Pao^19`6l-X6@@GdVR&ILc1b-+xUBy26ZPmNRpWO8@fPN6#JIPwL+T zM>8P1bK@^;+ztSV#7{le)z!iaEeiGm=YsTI^D$SVWKk2@(x020zO49anf54*QXW{r zs_4o-D`iQj6KYr|Z99p@ z*hiu<<~*nIu#2y4*3(=a)!gULAh9e*}+?b@{y-<&=EHs|tXn5iJL zJ0*|b4YqkR?)4snrOK}BUho z2s)pra9wBvrGZ{x0DGR5+l;eC6&EF_nO~N}OQ7)I!&0-Dn z5MrD!;hUfY2IiRoK1SY>u{g5Vr&o`klfjN3 zKPM+Ui^HSN&ESSEG3J7{u(r!<5tYGU3q-q#yvv!o%`a*q1K?Am72FD+w34`LEDfyF zwHS;|cB%{bCjSj-k*}a&TGtt@9^;kGWyyq$9%+P9zlXwW(BDU3n_ApO^-WqrmXQa& zleU3a*y>q>aKd6NE`0p&r%oL|Fd<-fdl$e%`hlI5dn;+!12R9s+2tPVHb{G)e)`SX zZ|@sSSs$9t;cp_o7gvW!8Bg_a(uyfvr0bVfV0+L~D$5vT6>i-i^>LQLR4 z+0Xr@XLM(0{mJ;Wh;;_1T-?w7+^^SlN^T@%kq|nuF{Wv0XHpZyiflcVPP9Z_gI5Lz zLCsck_I=r9nDc$})ewjU0^CU75QugA`i3uEzdMvGn;9lifZ$SY7Z4K`ipC%6LYu}~ zO4yM)N@Q^{bYn*eN-uy!7VjnEY>MjIv>u9XXn~JU%npdNh0wGk$EvZ^e!nYU^BVnY zHnv_@q~eg=kk482VXmWk0E*OJX&0SZle()skctaiM)z+RC+;aP>S^hn6bl2ia)Yp2 zIz5Afl`}E|e5JMZ^Unw8e0VPC&VdWu27`@|tWq8uhr8Wnl?HTff7^*RZOzuYAhcno ziQ+Jx+$yAuQj6H1)T(22ko~;y`kNP^q|3zflg|=y7K}~HP7qTxAT~12wG29y@nM}- zOrCUG5QE?R9q>W*%psv1hNT3-{CR%9bc;x}6dN16I41GMH9_nS8e>RhLB`k)=Z&@< zwD_$p>44uxghZ{bnJdjbpL^hl^UV1;ptUeJ#6j8w2DG7oB5>^UvF*tmVEys7D)n0F z)+X_HMHV*34%Eq7nIhdW`V*64_gW^$Q!wS*e+SXYEY#T~f*o7qO6ssND46b{E+R4p zihhkgl2u1t3>Po*&e$5f=_;=&M-7#<%O zJ-6`REm}=_y2d7e6={1k9gt6XW;&YLVPK`q%5EYicFyOUTVQ&}7Yqiv8)vbw?!JNd zE`7EDeUR`d=6cFn(>vFh=rKhF5rNesV$_zH< znw#4-wMNRCv%)nz^4uMm+mx0?bVQ!3_Ae0*Z;jBJ0x zg$vA=$T(mYC7UMT5kr@MH5LT`IEs19TP;A#Z0zN-WmL+p|fUjUk}YAP*5I^#pKv%F62wvyVib}+ znVfjx&DZHZ$0sH6Sh}NmB;w3Ch%^Hv6BCaSqoZnK9HV1O)wE8$N?ecFz}v4?6p~ny z2ZI-=IUGQS086A4H&!aW|1|;`kA}ych>s;iOXwg`2EbntZWdY~AmK+n`kBHfsHyJR@$B3Vb2QhaZQ}kWb>VQlKm7E~MIhA^;9AS0bDWT;YXOi3s~O#=NpAMOGMNqk^oZf=s%d`nRgK8)E&*c*lT zrWu$7u^t1c;mKxl20g;8=K?e0iU#pwxWNy|kt*x^*)HzLL5Oxu6)EJ2MhhLvF$DMC#4choO0&S|QI4)N zq;eOUvp9cv`%JduCZPp$6$P--s?0Ro=>44BgzKELm=FPi*Bb;C=#-EVqS?R!u`775 zl@+63s7uS+%UW4^Ez$H4(rTT>_qP3K2+UO6A}*6r;NeUNOGbuc%M1Q%MuqKZE{D~q&lOf z1FVw;h-7ve zhIEbAZ!bwAc~w+FBt1kBVa01KDq^hx#EL9>Ls@Q4b4v*xti@nxXP*dHW~h{5BE^gO zTn!3f*cm13@$taiAO;4hOId$^Ta~JKerH2EB3OH9!@uFrPTc#nkPYljI(YD4QVgEU zr}rNv-aqrbP_cj*<;zlf7c5Zer(xt^1_QttKH^$l4|*DNE01+b@ew~p3nGh$qAnWU zlxf#+AwFbBp3$)EJP7v!{*VU98>Et-+~YcNCowPpn=AJk$(oH`m1?C%try*SfSMEn z3AV^IEgU`zONmw1`GY(;m{)_`&2t17#1^?iiXemF$8T(6=Gj!K2rV|CJ}`TWQy5AV z{ViB6l9Ki!De{Y(n5f$&CIOgad-T zZ=5C+AqZOEB8^k2BK*@0Vg%df0vV+*&B!aDl`W;6k7sdVIDG}M1yA{+zj)Cf3VHor zHYPRtW?*WcEGp+UfI))fNjNu}K}_}vL>mB&x5fr1m^BU!d@xRN6U3}mWrCpEoSiLl z>j!oQIs38i2LsRfj;`=8qPS&iwAx)MOZ?1hL<+Jj={Dqhyd{VqqXQ zF)N7S$VLPBl(tUq#+EzU}b3Fk`E<)GTqH)K-9P8N>mH_d+fX&plID5Qku$R@j#>uDBIJ!#uH9AaIxY=yZ}C1wm}q+1Mwya`@7yj{YJn zIEHX0{<2&3JJ|_)6~K0HB|=;X)N?L#Cm|8N^)d1%yNHbjc`DVLnQ2?@-A0N4lNVV13L=;%o@!P}u0l`S%H-@Ir8q1c^;NL97!SlOgZl|M0`=g9q$OJzV|n zVM>g`u*WM9ZzwM8@pguCcu(F5WraVkZiD9oY?qKvl@}8%EY+Ewig&gL4CHy>ecZbK3w-SO9 z1QiWz0=akIuE>W8hoSjLk60vr_$`2?9F0{ziJI)QN8&KB9}%PUiZUKjS;D^8rmQ5H z8n~5+H~obw%YsR_M&F?@vdPc^{>tq}8k3kn#>!HwPFPD)fU zLXEzgnoYxs-fjtDLc?MBg{=U*t_IwDO?GQA&NC$5-}Vq>?u zl`w2nYV`aL1lzsF&S|yDnwqnChlbr z#L!3@9UuM5=OcPWp@nhUV?lYI~JV z$1*Bz49tiwpsnIsel$MnGpIW|cQ@IHTUnLL+)>!;B88h~?jujZNzBVw8l6AIom`@( zKk_&=62%QctbAo^n##zrvCh0=!hMC*EcbXPOAG9HVq)MWOpUVKj~P+S=TG%cB~2Rn zY9HxKKJ~p*E0z*C&2m9ZnzVUB&(_9ZjBMF~7=R%d4!@pUOsQT#BTqNSvk8XrLGB0@s|##lRaB`FUQIjKiUEq|4>?@(zeKYwcb$CXQ|t7_~2_?AQ$eafx; zXmrS>vulNBQ?_%8jkWLzY=c*1Te|mTlMd2KhoiKKLC^Um(#csWuBa>tVBE{O!A7S> z#`!L*wR%WgbZuqzSP&UcdS_m(t?L*f5B!Ks%usi*e_cXZI#=0@YEX3gBmzERFN5JO z9deqkPSq-V{IZd;%kBT|`A6KyR}&KtrzWt6`m3BhFO;Gm&bcUn0U1wb8!C*m%0M{d z4g&#?Rj*Yy!?p~8{B|v6omzK+7#yuOHuTL+QOf-8(rB7H>b|*j=l;mmfxDx_qf)8| zGM>4{j-F#$%MJU4P|LKn;l#yN)@y8eD@9$OTVD|^0a;0h2|VGA07SGi`vB~} zX0D(S$F7Fjo<4S}2{sp2V-;%u^WVMo?sp&tX4J(f^ZYu9#T_Q%M^w_zsI_p3qEE!P zAb?4lbcyOGZ5-~41~aKH+MZJQjw$D?ySEUhs>aZy-|3Dl&hYmS0n8&^^|+9n{-Mnl}xPjboh*maP% z8cGxc(@bSVGxHz)u=?mN<*@9D^A|ss7=QRMWgf!*Uk5SjJh*;gkmlRrv;mmRIv{q7 zjR|5dO6w(heY)1RwniOa(Q;$Ybo=Bc3uo@koseTay54k?v-IKpkpU13fS4RKa6OG6 zrtP)2DWVN(s`mEm9o#SQy445g`p2+ zQM0C7@!*d{73X1=QWmh^T75;auonUBg(J_i*%AALp3T@;*YudkQ^5C&>iH$Kv*1;5Z2@XjnD#kKSjR|!HeQ8Dm-R|rxx7&U5<`-Xl@%8e#5xyiK-GBL| zbT}IN`;Y7KU$TMT2J;BP17=mGO-DnDMLTAkN;q&+;m2B;-$CVy17e>Ie0F!xU~jgw zS+BNqZLdZA4{`bD-<3T)MP%t=eo7*M9XOmIgz|f-EyVt*JcMyUk*3or)T5X1sQ9MO-H z8draAi9si9xb`-MXEqndWl#`J!bXq|MZ=xxxAaQkr*HU;)@4<`&)&AU$lv1ekosI@)Z_7!*SWGKi&$( z-ODe%EG}q7GDG4=&JaB^B658?$8z?DNM~Q%R%MjZ&2>jIabdI#0Zb6PKQb0Wl#_;(!{R_AWI;3&Q2ntJhoF+l2$FCtcH1MtDQoA(!&2yI(SoRnn`| zsJo`p^0qc#J^!!gQ})K~O*)!#9@qcF>+$jN&mK7NSH!3#J{|i@sjrf{W(kov+M10f z*UNvu?fUmjt*D(w=A_2bBPmm5y$H!d@X9(R z1lAK>MI>Wsq0^68z&pB!FdX9Lq@$@A7#0R%3SiG6P#pW%;t(A%W%~_okk*`fm*>de zJ@Ld#N?4$%eR4Okv|xREFMy$-tDXWcvQSz^fJSCC-xQ4sVoRK5?j(qf-j%_x?!Lwb zGM#QpTts&!su5TgyTYs_wzlfkU{tdLbDcK5hrX|cKTv6$q!85GY@mH5ZY(f*_e)`q zFzBna)kU@IHQ@)VP;MmuwG6gr|K8+7FC5^!{P@ic zVjPGuK*EA$HwQBVfnYs=ali}vngB2mlcqPYQLLa${q>UUcML0~eF1C%$T;1Pk0R2^ z#Vj-6-g_*R`m3vLw#~C=`BY?E5x_!We=+1)dVLRxrxUSJ{Qlh;YnefhIA~4Y`etEx z^*q6T(v$y(5O>d0i0tljSn%QyGA9urQelV=@E8n4RY1b}6vk zx&LXOOq>numc@3_;!8(Hh6gwj)wu>eJh<@bp`e3Z$$hLw|HD?j%|e(l6!LEv(?ASm zTM*+5MaZqgps`r;(u#}06HxQdpHEGWdG`489H+4AY%D^4u#t*={bbu$CPJJ!PO&nn zwa+&6x9J3?iEGzRy)ia6@yW*%%gc9`?{LtoVMA?%rXo{~ce%p@2(yW8kv8;^0Dz;! zv4b>7f!YFOTumD-yu#21+(v8_6j^jId!1+`wz)KZ*VAm!Q0{=tLNE($ zzV@eoNs4(~|MP9j!VZcyupy?4R(6JlkP4SwYO{obCJc9%r_nuH3}^%juUV zmfv5VxOJ;_`F_>%{rmTqZ!9k_e=|b!n}aC<*i}#?D!KZfSBJ?O{)f3U{BE*5!}#VL zV;eoFC?W`cOp{8X$=ekBrZr8QDp_eNO^s>VgsvS;W&>Dlf(Rs;PEwf)9V>1?iB#4J z)Y*X*>9LX{Xf5u*j?HOGp$-4Ze)pT=2hZ5Y#+!x)({ngp-1q%F?{ik>_`9Mvlsln2*dELtI&t>nHWjKrwIAcS+s6YMo+fP4z{r#(VNj@-y9%+W)^bT`De`&?1m(XC4FT5&$;2l6jwypm^vFzC!O~4p6!Pm;x6PNi zg^~e22$`+3Mo0{Ayg^M$6B|vn#hh5@pm+po$feGkH?PYhVl2#SWK3$dFj!jh^B;Uf z@7Gj^$+7oA49E`M1?W`r^KUMG`6Z7q#FrNZFMMzh5A_j(S|8k$w~$g6422^7K`PmA zuQocTn4^fCWTwU|YNgItX;I8As?}R9xME-LxMl8CW7g`Y-e(#D_M}+9{YkZE+H`Y6 z%?;$aDsOg~EHY%vS8y@-0W$27Csy+Oy7Y%1-uIYh$n^|kl20_m-VRXZCp)FbS9!-mSPjrz6BrSlc{tz1=WTE60E_x+R=@_!j{ zi|ei&>|OW-#NK-ctcG(jJ~%i$4p4%XAa-d;;u3F(qr>d_1v9AbkbO>CGWhRTLpowI7il#2RO zH|jM{Ffz(Zz`b~2#mY6m07~qP9czm;ImpBVaBI+n=k~(TUjDb06F3RNIfhQNiDd#(0l+4DFDFA z0X8u=bN?ChLdjeMOX60lMd$wVSG zLIX=A(Xn}|5hYhe%vPFpdQAM0{Pb+s-^LRUX=OuC41&+CgUGR%=OJS$_Gezv+Wts* zCl{oSASNX@P@sjB!4gfk%y5_*E?D(09nE|Y8l1x-sb2Ujtl1k(d zsWh1aBY+AcV;)YVn#7(E6+w)un&fH&wCO}YQ`=CrkVTCHHgH1x&p~(!#m*&Ivb7}( zDVFtcW5wIgH8!GR`ny9H2Wk47c5*JjQ7^0`l#$XhJ1(0tH0VXbbA9>v@ypjPXym(v6dOyWCMQS8GXMjve$jjn_w)1p{lc3N)6j63%O8B9m(j0z zJA`{(_WaHBcYU$9w%Y=xW zElbN>!L+{4<3Vw~?va(yAnsiui(iqCw}v$Hq!(Uj<9YkqHTVP&3P>>xF#w|mQVEvTn?XA;T`;GsM!fVY6CP}z8I(}&oc;Wwq2&b8}Ad$9@?5#0)Q-1jVz#onc@Sv63qSVbB9 zxmNqTq}VmO7`&*392-rgQsU`>X9ci)-LoSkoL{HHtLIpcH3ZZ^29}8gL|aCH8nVwZE5}y6Xg{I&)llZDv-gsZhiN%6r}*xx6sE%M^ePT&k|%89jnI~ zatc&Yd@VSsFIA*GLF~n*rbL+2K=4vRQKtf8w>*f9kYSEJV(^KusrX-t_PYMIrNP%+ z8pb7x+el6JB5S1B?M99zD++l;1cjiMS^u5A+zZMfR(@v{{r?_D*oz9-nib18)T&A= z>%5%l1K5cRpB!RtdH9|2g}nuk(WR0A7H-6<0+`^I^QaiZRK{9LN`;%M#cR6}OqF`O zcriswt8rgZ!JuwE=3TmSlX=w|rjvP2F%Ina0;_l{xW1D1Bb8PR2^Xrt!w#%hRj`6Zr?~wi6KY{X3 z4VMFI4G4UaR%!7J0fEE~Z}{RccoAd! z+WNy%a7xT+8<0!iZVfS;*IBCBvnlj?NWLN^%YMTNVv4Y#A}#GAm88$57y>HV7nZch zu%lXuv<=5PBA`?(d4)|J17tcs@I@*!>(SC^-%} zo3fo2O4d(|FoKX}&nJQ(o~ zFZ*EWW9_$U z=LE5QK*mT3+J{40v99C{g#8b3Pnkb+_N)};`PVdt58Qy$xee?-w)EBH5#JiATK83Zw=@{C1qDJ&7?HMh&(;86vxO(D<=q1F;wz42<%)iSKWpW??Lkf1hK>rQ#e{uG1vMO=fP%Yb<2L zTskdu_~LCh8#J0Qo88_$2jqdl;h^_d?e%X|bF>~2w>$EhpRlBEhqyqatz3R zdH-Tgc{^xgy&mzxn6B#d#{-_ac<+IK6K9XVb_h1Ctdumd@kyQ{Pg0L+R#ejFW_>k+ zoT!}od@_G6c6)8d7Hk<|$Mk0gMsI#AH7^tAVMk~84Q(~O0(a;cW)et!C+k-e!Q|+#wpBlm<(or ziw_waQ9eiJ4dm#j-Bophm7FAkm}oi&3S|dtI#ZE;c3HuL3rY6qB2dQ`CLk;S%iH;X zH+5cXes1SZI@5+^dONhY%}x5F=q%z)b9D<+LeeBi+Y0ql#n+0;BcedM>aCI8pgS7|#=A+CiC z27xP+vGa(RrF`WwfjVfff}v>JfmrX<*p3(RN{*K}&P(~)RhAK&sM z^NDrDp8D3`{{LygeSfR&1~DVY3TBb4P;=cxS4_-A>~$7T%q|}JEmVvIT*|Dg_#}wo zh9dN!g`@U>hIr?(wz_;wV1SU3)B(&{dEQvrx|_2#O_{w6?WBoVHNL#0U#rS zp0gI=g}puE4+IgMRP123>LtTG>e`8P?yEbefJ_dzD{`lL7V+n>Rwj_uc7qtwvIPDS zAVY2{o{ZH-9U_3X)(KV=8BEs|WBuf=rM2W=={2%tQcw+j<{oC{??@8`Ge`R~MA}0zwQNOP7 z#L^#*gf%(lZU)86mYKOJY2UW7YyihRiF`TIT&yi=)?c$^7sMtJ;VwDDj&({xAO>WnTmJAVy5*Zc zJ|<&Xx*CcbJv}72$#{~t^X!v52OJN{kJ38N)3-l(0dJ zu)dscKOQ^ImV=7mg^_Wez|1PEt2G-6`IfxUe)-*?){Xw7^LWA?t&*v2`SHIpGVaSw z_Q|Dek?(mjyAQ$WypsF#a1eTWcC7BKm_%#ILe7RFS!)Y=e z0pW0d*mCn`O9=^^2|v5Xz<(^xXM zVx-fiXcaVGv<-TY{ISJEY6&UQ42c()C0SB*y(8R(YK#2NJW1sMw^Cj6pl66wW9poU`0R#Ok&Dqh` zPn416?lGT@Xq*)JSCvikCANVhE+y^lki>r_I^TqnSbJEbdlG9e7XI)KpbkQ|l7@E6#@Dbhn6WAQn+DrwuL<&^A;off*fKA1brx8X#6Lh>2e5kV`avd-*!GC(3$|tX^i6)gM-(XJj2?w|4-U zAO>XhNVa*L4B7gvl<{o7oQYUMQ#n#+uLx#0KD_ba??1f!?)w1trfmNLF>LImpSDE> zv6|bN&!@X55w;Trc8kqtZ9rZM#KL+=3(EfFq5k=!Q!b(zoi~$NR9XW{5RvqC7CPlu zj7dThn!fUh_-fkPK&xoC2)9=h?n-zuqIx8m-Wy0Y7Et6Can{K~8uYpiXHE5$)<;De zm&;PpbOL%-FG9>#l_8cs_0B=d1_uKmwiH;mS0QUU zcdRwPYhV(rz|2H#1jKwcwpuz;UJP`#)ywL5WgQnTS3Yb{6TNl`Vv{&;dYD{Jbb4cq zUPn9776LIK!^-M@iCb>kYmu!uA|gAPOp$nD*x*^(5|IABTQ0U@Av4dIAPI*wAauf^u1pNW_4x=f6y3@5q zUdOWsX2;Gkd%z8MH4&qXU<`<^=}O!Ra3~L3i{z>T17f)W%FuhNQuNZ20QZ` zj-dR7SN1k;8GG_iMz)D=iGK0ETC$Fq@F&GYjH+iMCBjaePk#3$Q4ARwwme;%J&14~ zPu}->6x|IEbAs3al`0Wzv!XjG8;5{SW=^_7Av>{%?I}?IqkD!r9xfHZObb-2foN+h ziMyrn0Mil!lP~ZK=?*1ZoFTYHr3~Sd*%U<{f8I#TXk!;wrC$KE z2Rp`lLF_NlAFa2|Degb@bf{=0i$v&mlv*H;8N|lcMo}dr?BBS2`P~D&S_Uzm%q%V* z5X8`8_q470VQo%QV++LxgG?uFq=)m}-5_SQ#yO#o24czthz(N378A=-Qi6z8!ezPx z0d0lo1!4kNJ^X`MOG~olIEX1`YE)g5{%178Vx`yska7fB5QE5p7z8#fYU7YQJxXy2 z#DJ`BWj~}$wmA!QRrgXcv~M&4pO7$yUviVB9F@^diTnTO^LG!tBaa;aktZ{n#%aA_%?%fef#U?SVzZ}lY&^ih{3$iCLx9(p9;UgD`^N3#gq(uJK=t0%0}MUfAMDrc_#RdxwB}R zx1` zoG(VP*lN%-H5BdNaumooKt_vvljICkRjWxyl|#8JLqrhItPsVxC-=Y0W>GVz1hGYw zETLl}X1VigR{QJ6@YWLiu$(v*~-{=v^hzo$XnOoW5Kocls}j;y)wa2PYT5LU;J1QgN(gnK4GTh zb7y+`1#GM>m~b+pg9xCdJ_KASz``Y)hG!0>lMaIZ!~}>tv%erlDp@}$s43$*MgM{3K^4>2zg6D|UQ%^5yler4H*pW9oj z-p*s={T)+K3@|exLr8pc)>;>3_{fn62E@oH1S%3_D#29c(I@-gJM%Fc7YO~5RB}`&?qnmMiiRa+zeiyJ=rLb{ped8Hz+xO-KE@GR2}#vm-rf9Df}AL?EF8cMmzVv3tXI8@WLsv zFxi(pJu?Ghc51dEq^9^7F+*54A9cJiv6>*Zw(8{3r;)?~C$wvYOJp?DEOR>SXuRA`P*=DP`H z!b$MYq|Zv{&*Dtl=rZ_eSBA4>AJoP~UR_DK3oyw>@Xh9@LiX$6s~d|uOe_r zV_@)lAFG>LN_mBSU|Tkd7~nj+vYcDuJo_AP2LAjHm$E9g3xwvhqFb&^$UYh6dL{Ab z^6T%vDf|3H3XxKxXcU3io3k@l-|$qWZIo=l%*ZRTF}}#iG}B$y_GFjJce;*9N@l5P zb&cT!BMHmx@=<@S`HAZGV0o>wzE5t*e0rdp4Lk+Ig5P#gg>Dy3T0}S!_Apf5UQPvq zxTO}#OZBx*FNBdXdS_skU8N8=Dm9LEHurk_Wn8&U#KijEy}0dhAhx^=Ud7^4?%5xZ zr~b(+b-5~)1eC%-2uZCH^hMbK8+#3(YF5?15%l_Z-| zDoUB^m?C$jrY7@J5&EW%#sRS$7}LekG;)iIpB14Ky={%e{XVv+ZM)dF7sNK&#(fUD zw_KFVD&&*p2X(|Wl$jjh6(ol7*Ob>GWs4Lu!`IKu-dRKt>m6C^ae_UNcV0kT0K{f~ z(WdGl0Bh*6_DIkNDh8>#CFr9F7Yh-1;8Q5>p~37Z$=ta-|K?A~Md+aCkw7}jA%mG5 zLivM91N+BlplrTUyaWM}+md=BcefOK)KDI%B=AcWR*H&-oCa}Azao^oN>hL}W+xl_ zE+Q!dYq@W{E5)+0=s3?&R#{>hPQ`6usK~&+RGAKm%K`Z9vvc$|x#25;cRl~z_x0WMz z33XK@xS8?BxUOSkee43=pt3C5?a%yV8OvW;5+kGHCt56mSg{zW1SClUG3W&eIbR>u zE^oii;x9DU?(;VEHYP^t6{)rNk1Q@SN3aJLCxv}$UdWgLCbuP@$z;n-UVS(yfKhC1 zYV|6i+_J&A1Ovh0u&ARaGS!I+Nl?VGJPGraOq`68@{JX=EG3LAUefINC8B`}VDNt@ zd&WUbxLAE_B6Vxo^N@$IsP?i@kSas}$6Uue+b%ZplBW%Yf$`_~{x&qc^DJrIa-2X$ zRu{?{0@>gEdpN(2a*i$%w(zkv2GUzoYfSQw{?E_9gt{Wtv#@yo)4TUhAb==KAU{5G zk5#y@&QZ7}I<@$KN-jz!8_iEj$t}C(wo66VWk>AxQFk0~;}sG5&ko%oQ!L)5b?l)v zYRjLMDnEFE0hwhQuPoWx+kLrGQ43dACGZpVrIN!fgI9`Mn)seBbQ{YnT)#FthC|hd zf#~|`_6sf-TT`1GUCkFSw(ad}jIaT3LoK$a#jbL3`na^lbh6@Hd@EPx6F<<4933mG zRDf*lM$@Av*}k@&O5(xAdy5PA7C-&%-Fv*D6vTe^{yVepEMD~t1^4Z9QUzThhIMmf zI8{2;v>Qw4fOe^svfVCoQn9!&G0Q2QHqr+oKq!J7wk`m;8q({j!5vT=Qu|B{4Gq~$4QgyO8MILRvf#?i- z4GH}avZL9UEUh51lQ>qN&&E5YLCY|OO?CSTLS**thMbXuI_B@(L}f26}S3mE8AMQOBiE;#3ESK zylv;nK3o!Z@#4v2_S^;$`|j3^t^yi&kT*&XBo1Ij_tQUk<|n!ml#>#y)_A6|AH+JZ zpMOn=#lh*tMV#`3uP<@l@a$j>u>`lp6H{5IvqT(gHyXsaM(T)}4IUd~=QY1y!7WE? zPHQf|3_a!POcVZErT z&b+V-(}AU^BsUD6UVkdFDy;uPGdnZ-P(c@|(&j-O#5PDRpg8Dz1e8^;B)IWnNDtLU zk>JxSL5cU6R|-kD*49sMbe=!|+4X5zddtV+{rmTCUuDq%?}fHfB7|XS=FDJEyA%Jb$-Eqn7b{Wy&*!Z8WC^iG`x-QDZM9P&*Bo^|ARs;1byn=l z3}oij*&vJzUyu_pYqc`ar-$9Pe4$?KfsPcqrCW*iT+E*kGS*5nq5pg{r|}!a+RA!% zW9k{itF5|SdTZ4yihb>ok_*8q)KNb@@CqSJ7}%OaVt!<_6)oJ?UIVR#g(K68Aa?gI zfC*yAe>{KY{ewpquZ{(a3Fyb6aJ01N^Pj9tjh24urJ zKQV=kyQ8H#8@(l-DYkaj>=kiyBz;AlcpC^rqk>pa=4v(7ms=?aV(=IlHn4PAQ7wlk z6D?PpcuGEaUKRk$)@x2%uU6WMLd(YTa3E9Qde!-iwzitW##B{z)VsNrHM&MA&M4#0 zkEDhwUJ%6YT^%Yitg0qk>iiYFn64+&$yv#c zYs>_vn48eDaVtK_>r-3I76Zm0WJ4;mqI=-T9cE%0De+0On*?HK2~R-AV=mQcbN}&L<1+EKP=Qdu7n==IWbBhilp5sF31}<*cV(w{IqoQd^5tXGnasDX8P*Yg+GXmq5U`J zIr{2PW#9EHzdEw;)t*pID;8<|Siwv{qq>Dym9MMB*$0}5M=90Uz(pCy^tzl3k=xhH zYhHOG-x{t)BD-a%=hm%VJ&@I`R-D*9*1IbMh-Mk8DSlRur7YO?xHs+D@>Y)R_LP*!`V-hUp zYcV6g45mu0jAOA`ijJBZ3XD3`I9)|jBx}y*b0;)Pgw-<7n7wOJI=r#GS%wBNIo;i7 z1$nwq4Rn)2m}5|pF-LJrunR(AeJj+(3x^8~9*p!bGZoVIl_Ru}>4*+qzwRuGn`9#Z#hCc<>||b(h}w3cI?> zuA*t;m~6rlb+IW{3AoT0KX73BYNnnhCJoS{lE^VEXnu4tJtoCzokx;BCNK+m2y7{q z2q)}zo@L2XHf}cfSWW0CkkxNLbVAr)0lENYN>}Mw6DbfYXQZi=2s@Vxg%#>)l$Y{x zN@GsHUy8I4;=z8_V*eD+v)69l{c4OFA|}SzTrbP#*rhhRFsCJC(5z$yElH2C zvNZg_+67`=fXRVeL4C7oZT=Yi(F!AC zBIDL_3c2f>k;veSC~dOlY39<{%g@iuTorxl+iaP86`!Q*fI~Ne_%B}}w;UT%>>&_i zBLL6Qq=iO#5Quzn;yh_*7GsdfXoF!q<~%Fd)oSZ@Y`Tdr>k*qUcFJGEDG<{$8v0bI3i%a&-Q52RZAfBDSoIZ14 zj2%#R#c698reuBkFnGZ;q?imq(%)dlfZ2%p2u_IpS-G_nz?vkpWMf^zQlIG&r!@J+ zuz}K@(dA=9lrvXSHBH8j2csx9%Cqxv-Z+E|#8)i4+b!J36cQsryR`P!X&`O8a%EfJ zUZes#n=XIy$%p68p4;`@+{0jqrbl{`i3qbg$dnRM#wRCfi5t-L z6+F#S3R9FtYZkEREpb{S?zE(ofX0VegvEiESA5HH@J;4?@(d+t$0f4>h9dMJh`FtK z>x!>k7W~9Yx?P59yc4bc$C_PNID5?{?l(yBW9h@g>BptZ#h&WFe<)jZJlkB=uuhXr zRY5FyyzY}HiT1$7VL{Og&x6?g`;=cVdB)Iu6}XV(6rllhQxNEJ@%T^IeIoO;3&em+ zV!vU|LiuuarpI3$bXe%KU4yYs!>Xc<*giiLk`oVN0inNs%7kQl70- zJl&Xrk!k7o4TtpcFOad7yrfeA4 zv6DwyLvmZPNuf|TF%F33b3TqW2|MG1tvwgSmU-{022e--id|1H|p#f zPFq-yLkkPD_dmV;^;fJN zILa=zdES2ao`lW0JZ%U?u%L}SJwbO#l;i3Rr^%o7$z+~C_$S2nbo8F@U)SSZ7Mmaz(li-; zt+H_4MKPsko4gHpX{;3Dp^wC_G1Igji) zUO6)(a;}I~OiR6*g7sHljS2C^Awi5A6Q0Q5^DOu19At^IvJYx03Nzg#IMKGf1!DCg8Ql@g2*_xoplToj8m88)SoY`r z)gt1H$(RIv(AUc7bm7+`UupW_~pV0Qp&rx*_lOlRF7eNdR$~A63j@;=T|Rmm%-j5Oo|r? zG&v}D5s%V{;zH6G?cxY4jh9N9utEcp3H@jg#3*;E&SAj1b?fXcdc(poKnzNsk7JYZ zE+2T2Og@zL;7c8>y6bH|I+4Lx8GQ_k z^WFxr%Lpp`6T~+_07eoHw-_Vr_nzEyl<4QexKQXp0qUe@+dSVqCKiz46A;@~SNW4nNv8T~ zR&fxOG;mPZf*-5`W>qz_6jEWhwlvx3@JF(Y;TxLKEz_-W{@@biLaMKt{h`VE2YCy2 zY$6=8X_^o_r943dbD27Wd-2KlE@NPyzyIc2zn*<{K2>WuNA>lwD57${{q28;!uH?% z!_WIQ$)KeH#MT@O)Z*mKomXZKf!L+5Ftig3htMYgFzD+g;yxjtZQUHOnsa$hdf*tY zd3qO$OnB+Vl0mgg_*Fq}0m>B|`eQlKdo>YL^>7#z0s(oHn6;H;D;C{z%Wyl-gnJl4 zs3T?=;?cRKqNam6*>sQmMIq)gBs zJl4!;1&Hkx+WN;o{?o_r{5Jsm$AeRf+On16>thft_~G|>OZVj7ww;J>ZS#12E9S%O zj|TX>dGqtnUq@1T8pMcX;+aG+aY_O|hRri@;1**Z z>K%LLIG#P6#<`*X-ua^w0Xu=ou@rLq{qACkd4d8jV8qH8nHK8KJIlD$vTOjG4Z~5T zY0D_+65L}s9yhuDZYluu(BaS{=Z1x2&p3q`a))$HFprHK(s2#P{Ip zyt`{o@xehyx>VR^Ay@Lw!80IM z{ZZ}*xuv`Ud8dz_5XFylqz{In%(j;LJ&0ZXK?Vp3^Cd(etp+h_{G{SW431&eRgMe9 zfQleBg5mW8jWH4GLV}Y;L^d;k6vM{EsW`in6s^GAKs{1}Ub>3QA=6`|r)3vp2y&f!zoMrrg$N93 zSAWa>h`waB8%b@zi_fC4Z8{^>`HGUo6mICsB|NDd+TJ>bsr1xhWrDl2WZe&U!fR$5 zT(>#uXR@ESBM7aRT++LH_fKAW>Dgyz-;y;8O3?i{i*w$XII|h~;Rh47A|1cwNq+Ub z`o$|O%bf-;_ z)Mrncc9T)&{@`1NsYSFSLMY0e9^Ore({{c#n4o<52jk`dd09N_EE9@JBo-fGW^<1~ z_S~x%)FKZ612QxV-#HIp4|J*jS2k_kQs?EtGrwIpF?Zv}^=wuE<5fPTHch_&u|%!P zz*JnpU|kHL@_9^4Zsj-@3TQrm)Ms0AyBrR!ga@mZc63}o8Nd@YO+C3dUMgKIbxOw< z2XBu6SSx5%6?^#D*NUy#BlF843trI3n~9MvP<3MDz@zW?^oJA6mZidYG#c<7VKH1# zOKSra8x$uZv1A*svD;}*s*num_p7i49l5U$2p$i1iTm;$H`;6Cc-_1HLKOwRf-U(@~6e4XI2cn zQhHTYEYTf-nW{nvV-JLZ-gKJxP8k_rT`RrIVP=3rbJJmC0*btp={QCmRaOt3z{JYU zvW80KR1^(af51KDUaBus)K4XwNPHFrikjij1I|Zq;OFg5rOPJVP}Yi$UX z6v&>EqNPZCv$&5VJH}W^M)*Y=PNIzl=h=n;s+HYyDC zhzl1gJv_Ajb|iy$9aexzcmZOPLoW9qo3v^PLl>h;?shG^(GP45miKA0&!_BYhI?w- zXi0%MVyEp2v38v&6*{1#)9X8U8+mI2?qG$#@&qKkYy#eUVyp@nHg=LTiIUu%uTvGP z4Q7w-cnrioxP10Fx`^m5U%JQvFxDT*vK7Mo3+5_e_Z zUc$>mS(D^OC(zLWr?ss@R5b;LtGAd|fD3MzW`F=F2Qo8_>~e=_<1W#rEr;5P{XHJi zat{an&KA1#>kL$NEIFL4$_P3T&08w7;~*&QOK;s?1=v*q>>QfD=wT9VpZ{)+NT_tj zcRY3i6FYuV7PRqyQ|q&2ZwR(^<_ymxg(+@9zKeKUWGO@gJKztSMfvaoj;WlbR1fE>)9zkkN02>gd8qMYS6#I_`3+4UIz+&pM zH&DbN3dWnIKr9iOnJIU09F--!9doEFh=X#4htiAl+HR`UWZzfOjXr)(0Y<|MCwnkP zj#R6Qx%H{bqmz@9w{L%RoBrAz7Wo{^BzY2T1`NzKv?^o9^I~H~H3(WTXn_pvwpc)( zP8ye9l$LGO1{eFWu3gNC30>SSv3gI5J4ngU#FOY!9GSj~frdY9N$7(Nd=fv3{Tb(DB)yjxUw(2$f`7YPwWM~uuPUv*r`MGMrhbg@sudlx+Y^w64H4HAp= z;>rNC!-{v=v4s47v%}!%0FzuPfxtDPm~vt*i9|S@gFlB|x(wxBs@((7fbeE%(+sIF z^{^|$qi4^)3t*VorBm-uoPV)RHY>#1cGOJb=u6-J4uZ>Qpr3~gBI5b6q)h~HT~-R9 zMffh9h-y$6Y;wlU&tPFruWF2QHOW>g#YJNJPN6;vD+E>-!%Vm^5Nnu+y-fzSysq)! z_CjL-F?@HqEXGxFREPST8I!5!T9)@E8YuY)S~_s!zl4Z9QCi5$xEygoj8Pks=1Nc@aChgKddMk7ca+RhHidv;k0snA zfxVRqvru5Ff%Ujv&T#)M>smg&Mex?GjxeIFd?q8pGVu1UeU@84GuC zZOK6B1~Wqe_TGi7g4fwMCzXj^l=ab#$Xo8IWBO>@Lp9WDd-UmTq}l+`4PbCHm|j=n zlXBH0P$>0U+@c#&;lW}MD(RJ@+HfK)W7CTEPNFd#wuRG%Y)M8~GJ+X%nPR9`W)gc> zm3BV_Tg?egfu?M9WCV~n%?dGbY3reI3y2A52-@KK@9Pk+Gv-2I1_^q@1kE4TXUCjU zs+zs<-tgH`*2IX30T?C*krG_FaW7Q%pscJ~Lnl3UCCQRSY?;7od zRejdJr8SKkWGl2u;|&W_;RT2VeC56(0c@vH$+Z!$lUW);jDJ^xN2IxtZIcQOHYPO( z-T-HzB8bIDrhtseI3>bdDAYGkA7Gb5NDB&tE+3PrI8VsftOPX6GAe>ywZ_0JyzNiynn=? zZWnq(YmUJspWmCz02p2@)0hNbiTk;gi`WyZ0aH5ElR3x-3*m|JF{>7I%Yj7CJAxlV z|AJQ|8mBrqH4^nZPM?m)1udRTq+2YhTrQ=yLGV(lHF!o*@aYeSHn6gp>GgquIS#W< zYzm-qFnU}Tswp4#F?eM%)jF0l!+gZ%tzWGjVh=M-fjZ8RTRa#h7NJN}ZDeyT(MDV! z5CQXb+ep;h))=dk=D3%3LAMjU{FJ4%)orR3yToUXGym&;jZOdvKR znoHUJbL7Iq!=tB;zDr?$5(B$_-uKsRkOMlVA+DIyghCG=a5d{-LW}4#)Fg}9X{5?doBC=hED_M&0PrV2h zhfPJh)h(Pu9KH!8wD0GXn|Tj$^6Cu1_t+NDTdbzg=Q2O+%dF@&~t6%14pLRseM#(gS?g0$EY&v@nt%qeh_23 zSU8N57SR&~%L;i=foR=Dv3aoI1hQz<`HZabjE}_QQOD^qwns5DF@rfRa54Y(*HkLk zlH*Gvbv=bbwA|8ME~IQs`ncl4j#eYrHivfWTp_MxhTs)C>5I}7Q_F0eS%n?!SS?k`v4iVNRR|E75H}w zBfMOpj)PzCXm%6^J2Z$x@o}y)U5;+ZLyhLCsgb7B^@3RQ{tvR*h1?9MYhgifTM+mX ze4nCy1A5rloK*7=Cp=sA`LvFBB=5*3Th)XJ@PK3eN0N=QJS_={t2USNsUa*354Kz+ zDH11!k;srZoe2t5YZZC^E54Dbx~Vv;n90dW0p9i8 z>kHRu4$>izfnausvO31akvyc4g?x2v#cyO9sy4b%J6#6l@gY77 zU)8P`mM=!q(!h?Gb%`)L>I*(UMbA2O5}$A5xRqp`Izuuk254*smub$3toySs9v1PP zjZhhxT;yQ6jsE}FcD7G#o>`jT>Dk%L_VjkLo2}k-c4}Wf(FlQLAqim#^aLRZccmpj z;w2#x7J;xJ3%S6cFm8}iCN6ex8gC4a+b_Z|8Sk1(>`Kz(*c04YJB2&x4`Vwk8UzHx zf3m;p@%R0(rf*}CuC5Ajaqe@Um+M@|vBbMbgTrtZGcqx0wPLku z)V5PRrkaZv&)>T8k25nf;jzU)BD-ZT;_0mM=Y|AAx2Zh@E>Mz&`r$ zQ}iTGr7vhrvRHt5h|St&($Ue)n)ir5V14z^Vq|~u^PRDi>QicVfZ5t~qPriR$t_zh;+&_m(>_OGW(CVkk;9j>e zpODWld?0@L>2TT;9?RzQD9DVpwM8PGk=1Nf=dD|-s|dNszF2U@7B$(|?avsuE&|HTP*4v@w`aG8ljAQ~d2QMmSVWIs)5>1` zkC2z@S+mae%8D}3e%aRdj|fX3OIIkhd>cPTHfXa@6C{(IWB^8s#E}YU!yh`WRj!2J zDh6wUYW>9-L>f=@rH9i~u}BtDbHK0LnT_1bu4c0{eOJ^<#k4rU^*=u3J^~kNd$9{~ zLsbF&5*&Fad3Yq2@^~Q5N5=ePoieihSAuO;Dg*s%V_k$@e2)acbCb;5RP<& zvAx+;C_GcF0x*2ov{w zRB$oO96l>vMHky|4suVvX;?*L3s$wA=gxuGZ)nO-O+j8ubudhjiKM6QMI!ei*{ry@ z#{?LN-T6p(;lS>m6mM&wJNS$?Qz3+n7+(?+b8*Ty% z5E~qPS~I^uNdcvxPOHOJTH|hNePmNpfpMgwnN8WW{i9ikx#w1%tPooeVc-N|@KEdr zF*F=5K2=cp%3*t(~1XszhS()~#E|PMpI@oj!GcYAQW7 zG0SG?%)Ri`y=)f9VnXZ`+v|Z$;l!tBSf~KybarZ>uW!Z`)ghqf6>%Dgpg$6cMPfMr zSOoNfUHcwj{Po|wLXEn}k54dGTRYvn+cLkzZ{IKy8tp7Nx-hb}r$zpy2~95mvy_@c zTcuD)3#6LoP^ZVp$f$Js$rpT*=GG9>lS5*;&h5E3$ia8PJx40Z!WMg4|$(o?qTy?$E<+>hU zAbQbuWwpSgp1eR_uW;K0<>FnD#O8I`?9>4@Azw9lxK2n38k_smTF*hs8C#>`a2+B3c1 zday6Rz$kh{$woNfHe^W_7)r;5CH!K6bow1C^OP8lEm*7Kg?v0(+zp9~j+BD>mw5m6 z?YZy2|7W;{&@hQIOoj?PD8(s0qJBCf{D*h9>gp)#PZ~y5A}GlG{sd6@6#!gZ?K^$$ zeQq2v>7C1*KQPTd!5A#F*31=Lp2F-t`}oZ5GuJ-Fyy&LhO{Y^l5b5DzKHcllP@EWM zE}mb1x8GAg6He84#Jbu17>HDL*vb@_NkDnk$!Yg;SC9Df% z;V8MrjYPs(R?HF}^Dx z$S^M)8noZVG4ABKbMjCCc8%#VMu#(uL@}?Z+X-U#FW_US!-9%i~G#O_}4FwD|X+}nBvav=q?c@L^c6tSoI?@ zalN#ZTO-(9Dj*z6Hy7fr20xM}hac!RT7CWB_j3TuEJK2u8EqT@u`-o@O%FA0(Fn@r zsum_)(u2gbh{eo1dF#sYbIPl^lT_+QiUsqGj|eri4`>KtcESvEn@u52Mc0ehK@@sm z%Ul4lE@39snS_5VJi|N{uuWtl*}OI4=t_oOeqo<&3}?;+1BK}wFWSIy7g%8h_yl}R zr+*%gO_1+0mX;KRHO{THPcwxD$lA8+4Vn2nXdQZnmuY2~m?$c!5%BS7yK%D}{ym-D z-8yBdOCXj=U|rZ4H}eXW8=|b=eq^>bE_ipb;}YMImm)72J1L`+&ma}d&?h?UnV2A= z$BQX&OLu3WY5^IAO~cjc_h-X1LJUp0j$kC>OeDzaLa*#^NcJNT!^W88HCKV>;&~@< z8UDm#A|mFuz9wo5^9xI*P|c2au+_JXyoq%IcCC%RZe0Vp7K9=JHnw=488y{z2;&J#wY4hddFgok-5J;O8E>{z6&tq5W- z{^kD8bNy`(Ud9YcbKa#yVcg*?kkVH9i2!73iw*%<$IYx+%Qe{C)hIU6sBKOvEVED> z+d!6u&beB;KJ2jb1>M2~K5@BPAu*v0zjpu4@DE;wQXJ(SZjnS@`9X^GZio)jUM_PQHY z*ZIE;^NPn(sTlUv?UAX)QuVhY%$Zm$`&zyJtG{{Wf*wCSl%=?&xWb5936k{Xv;G9f z>gd|dS#q4UwH=6JJ|U(}#R_CYL}R4zVq}^Fkz)8%>Pk|ME$fhuV=j&DY+`1&7BY=B zbqfnjCeOoT0x4j8JW6MEabJ)Z9FdOQ>{GN=2S$5aLrH z5Mw~RyBqhMa5&z4ix{kgBHAXi%@__+KYZ9|otxA$>SG|{UN6%`-)qHpJ$fV>HGj(0 z+)9iEDHYaz?`+XKds;`eb^hi>j_`s60AOl2-ex?}6DSSBK_2Qa!>C2Es<)l5v;oi< zTBxT~>?$}xjBk#lk%{XG(oTsZxU5Jg>RSb|dOW`=CK|4<=XScfx@f%+#O`DL_2MqT!K>DUk8qG;K86*{v9=wXYAAu`w))4zQ(}Bd!!WgUMZcEn?N1 z@PFY5qrt46uPD|+3}PngNk_B|jgOkoDNh5}?{1#IVv+#V(uT%#cVaf}2__|~UWuJn z9BAD}CaMmAR6vJbJCV%%7ot-w@8+*SZ3yOzw}tKbS3{?UhYRXqM0(8f}AiH;4_Q zd{;XN;|}{k7P2v9S%YMv?Qu^5>>?iQO9=R!zZXC9v-EI^h&2;`JlIS5w_IKM-fKfN zrOJnZdUhQD8xpbnLs8oHr5vD4lAy}#D-_WGks*^BNvKM z+R&6mX^-z$;o?;Qv#hn0U^y}JjpGmEy)f+X4A&9|o6koaNw?cxHpgw%`W2sIn||Az z7*ZCgwPI??cA^uOZLCXNFcjhgCEjKHS1rsh^eorb5Hhy+-Z*lDQSM6*D+WS3HIX-BZgopt71N+Abg z*(`VwipA0%GJ>g8cO>t;FxO0mUaBc2UqOs!e=g-u&3accmn#;ykOAJC0JbH>JTo55 zI?Fc{x2-b>Sgm0{v-9C&8d+2pt#SsOU6EU~ZhaBxTq;P@&IFnc+~<3ZJqYdP19 z8cP!=8rNhj{wu;22s%|ljAh5@LYdL)Ni*Y&Wa!0BHz#{~EDB=xDxpGMi=p-Hy|P4KFfls6?C#MR!#Wgd@te4n0*vKKP8l;3sqm zD6k!(B&psCU$%@FYo)5b$+k>oL|qq}iZnwc>VknGg@av%b%uwjW1D$;ejEY@zeff) zdI%q1{?R_ANUt(*x2+{_Dn)3O%c9F z0s-Eg+!p^`h{Ku)fP?^IWu{!^4_Y~H6h&+>gLD`jbdj!)a5*y(%9Ck?z4)LZ0Aflx zX*{ope`{-gEMn11`y)s5*FTjIk1UKqXseO&kByOMcs%Lh0sWc~N$+OzXQ6Qp)juE3 zg)oV(H_dL!wQSgx5WKEr2(+YQSXbI=*@4M}WUko;A6`5}GBRRCl2 z(wFC0QYc#d?O6{m3_*=S&m3R3S6TVm5GW~3@g3ry2%|Gt$Kr&&y$xGv)YLudK{vt` zH4zIsK|6+44tS;00&L2Yj*aU{;$7lKKGt*bD&_utAzT;-g9{L4Rj}_8&m2yRyZtz@Ett9OX53N~pB_yr9YhE^2>?EhT znJuI6Eu@Y8ROY1xm=7;~4q&$g7|VlBe0dt~7hdeLr#_Z<8lBYYmlj&~>~9ajrX7;V z9YHV*K*CdQ|y-`b>x z2cHywA&SWUbm4ydlR}~Zuhlvp4`N?4-4xaoK)%JeW6)ZQ2xU6Xc;t%=ctml*2Zhf7JqQ5Y{+gM~yQ6(f-( zmXSZ0IyrEXPK{?`gld^1M`!^tSXHk|6h+1RQ8w;oGblwhGoGXD@mPA@4K)p=5^GP% zL78(zA1aXHIb!1z6;Lyk zYhp*&tNX!Lf8pnblWt!o;Lik8;b8*Tnc=%yxFrXAS=wV65;cMTeCoK>M-z^GE+S?hG0{G#wMM`WZ6VnSo(?rq2+Y z?U?BB2k|eilG`VdR>>HF8REf^1j_4ZVK-ZL8WDU(n#M6%^2(}U9<1?;#q+lhhAe2X zGpfMH=c1WBc(51-uRi_`;KnHNFh}(5{g^2LS}>>>7QL+P;_HaErj?(hNwn49ZY9~! zZpMBstYLp3WsJvf_CytsWpqFcu!I(e6c6pff_F|STyky8rfOu67{JA~eEyZ|uNIsH z6yb13imYOo1vLQH2V?-2?&wy*E*Na9+I5$8ORz84VMr@zvnK%(i$X$;@?oN@yNV^A zOL%n@C)qN;W_Q=JYxT$x2z1M2MM7*fix?^fY#t%TJNfM&J7O8lU63or7Zz}W$fQ<6 z)rL>Y^ob8EV>p<80fS+!3{ErHYU8f;JU%MbxM*~QE*h1sQIbbYLy28iudYeegv zUvd!Lr0ZRV;}mME-Ry>E&Ii(fc*{|c7GjZzv@tm{ zvDEBDra-qhiP-K&$ZktYwb2MxIhri!-90af|K;)M1!F?(sya+oCiNqghWbG2 zk4Fx)RaNEOL%r-&L6PT|Z~W=+r0n|W^#ej|Q)M-#?KPrQaO4`PW2|W#1_f%31A39H z7h=Q|AeJV9QLrRGd5qOd{{UiN%}(Ss+f$G#uLZQ3I2SGG(7|7DFNrTmE2|HcF)p$t zA9lUHV)NEej))tY*2-UCJNHQDUhM2;qDP!RX9}uaTv&LN@;Vgo`ET61CQ0T}lP5+!%ya;BYCn=4ai}K7)h(8aBWd&K`>b`s*I>Zv@-8#UEM;BZG%-JqxKs|27A9={+dj3 zb#+FU9kjGQ?)dTVKn$<{M+IUTdTU@FkG=`BxPgTM{aWXxS>V@y%^X)XQqNq;Lx;GZ zgAGT~B#4hAshyWzTwIBz)0rZ;NOL81ShXWrWM`FgYlA>!aF4=zNxF@+S6@Ww^f~vx zy;#c4(>wGCt^s|VI({6vi#|*oKXz7B55sR58;_8Huc&+nw6x;qjbErl%Cp~Wl3{zA zaQs$iPF+p8r3Dr|E#$V6ERkRwW_gcW`N_jetE;QWwDjyd>`Sz{JKtP`5b*6EM*<+` z95M1Z!Y=wRY$a%@eS(3#V-5xokiCs|#!y)qImd1DCK1`_;+Sn<`h?CA-pbSzmr-79 z7QnDtJfkQzA&Nt_<_n}8PEF~Te)PP&u@^^|{NAN`$+##G?>iw$m6M+h%tACuLBSF= z29dI9O($u=I&kScsr)}uYi$h)FgVn4>DK2R5cQYlOLg-LHBU>sM|uZ$mX`qxhBPL2 z_0%)OOe4E?=i0U3|Ni#~Y4GC~;v6Gh?3%P}V+}3#N4&*Y8JKB~j*R?uA*SAXD=jbE zZkJAzMeWbWD35-jGs)<*%XRN|T#2H!om z3LB%hHh=gjFQxXDh>gGe@QVkl-<`Y)R3}ejVc&f785Z`FcpQtOB1K-N2t24<<`fh%*xi^Zj!Bv}u)gpN?%8Kp>l!}R*M_|KmHSXUBt z8RMWh&rM{u*h{(AKf<(6@ZPr*fI1J8dwA zhfiENp0#cTiVpIK9*Cx)eYl@$zg9Wbkkgz*+?zFMeoA|Z(nyNb`m~)ydI1P95GbKq7 z8|qWkf~#^Zdu8f~=C*G>_@ob$C3@^67>w%o(*zxo zD)R>N!xkmtafkTTnekvYdv+r2={qr?YsvITM~2dZwGed}J!R5T+JD-g17L3Wr9b_b z&)0V|2k*YP$#^<={XGYc&6$reG`zp6!1bwATRgEx37>O72*$XRW48X&$v8bsuR zkvwt)AhugAy>Li>;>d4+SFQVJfAw7Sfq!%OF!UM?lAVUWg$xzLupeRzXU65gU zQxm}p)+BebvQoR;v^3CE-mm0R5`ima%Q-Y#WNyH+TIIzM=&Ktgo3t-4zxUoUh<%5C z5_C--NC5j7eH&?NnA=^7Iq5M@cE&{maJb^6N#4;X?^e8PZjb{uJirgn1gFFh?O%~F z3!*rr6g?7iRFcHR!^9LJKjkrz8pY8b;lg6JjwU^_X5LWCXEzJf%C8`^^&E)(WGp;z z4DWU4BWj`i;rv#1##jc5ooUbA`WQJ{z}8QMgo*`A92Aa8rIhEt`T(goy79RJ!ynj} z;j%&jou3!waIt~D_i}IVC!Z{1V&4d{4!Y8hJf5XuHG|0Nq?|9=ttuixmpzzl?0qeFd9s4?`T%XvIKa5r7w-4@O^ zn`_+6T|4YDWvf0il0wW~Yk}TXDnQ3JHy?JkbsB9N$GUaib-k|chFKTuTvdF3=hKe} zz<3`Kf1_$D>o8SFyj;qzt`7=4nFg5UYFb*}di2Y8*;it2tKu_>|GQ62;Z8`SbDU{~ z7+w%V1Sbw+@CeSvW+8=l}o!07*qoM6N<$f`WRQng9R* literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle.png deleted file mode 100755 index 17177f3246cafff0cf81669bca7efd0a22599f44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133664 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 O0000 RequiredTypes => new[] { typeof(CatcherArea), + typeof(CatcherSprite) }; [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs index 02c045f363..08bff36401 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs @@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Catch FruitGrapes, FruitOrange, FruitPear, - Droplet + Droplet, + CatcherIdle } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 36164c5543..af7c60b929 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -44,6 +44,10 @@ namespace osu.Game.Rulesets.Catch.Skinning return new LegacyFruitPiece("fruit-drop") { Scale = new Vector2(0.8f) }; break; + + case CatchSkinComponents.CatcherIdle: + return this.GetAnimation("fruit-catcher-idle", true, true, true) ?? + this.GetAnimation("fruit-ryuuta", true, true, true); } return null; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index b977d46611..2015937f2a 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -155,7 +155,10 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, }, - createCatcherSprite(), + createCatcherSprite().With(c => + { + c.Anchor = Anchor.TopCentre; + }) }; } @@ -205,12 +208,11 @@ namespace osu.Game.Rulesets.Catch.UI var additive = createCatcherSprite(); additive.Anchor = Anchor; - additive.OriginPosition += new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly. - additive.Position = Position; additive.Scale = Scale; additive.Colour = HyperDashing ? Color4.Red : Color4.White; - additive.RelativePositionAxes = RelativePositionAxes; additive.Blending = BlendingParameters.Additive; + additive.RelativePositionAxes = RelativePositionAxes; + additive.Position = Position; AdditiveTarget.Add(additive); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs index 025fa9c56e..78020114cd 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs @@ -3,31 +3,35 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Catch.UI { - public class CatcherSprite : CompositeDrawable + public class CatcherSprite : SkinnableDrawable { + protected override bool ApplySizeRestrictionsToDefault => true; + public CatcherSprite() + : base(new CatchSkinComponent(CatchSkinComponents.CatcherIdle), _ => + new DefaultCatcherSprite(), confineMode: ConfineMode.ScaleDownToFit) { + RelativeSizeAxes = Axes.None; Size = new Vector2(CatcherArea.CATCHER_SIZE); // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. - OriginPosition = new Vector2(-0.02f, 0.06f) * CatcherArea.CATCHER_SIZE; + OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; } - [BackgroundDependencyLoader] - private void load() + private class DefaultCatcherSprite : Sprite { - InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle", confineMode: ConfineMode.ScaleDownToFit) + [BackgroundDependencyLoader] + private void load(TextureStore textures) { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }; + Texture = textures.Get("Gameplay/catch/fruit-catcher-idle"); + } } } } From 9d5327b1accc07234637f51f7903cacb2b9966ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 15:00:23 +0900 Subject: [PATCH 174/387] Fix osu! shaking instead of missing for early hits --- .../TestSceneEarlyMissJudgement.cs | 70 +++++++++++++++++++ .../Scoring/OsuHitWindows.cs | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs new file mode 100644 index 0000000000..27a32aa96e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneEarlyMissJudgement : ModTestScene + { + public TestSceneEarlyMissJudgement() + : base(new OsuRuleset()) + { + } + + [Test] + public void TestHitCircleEarly() => CreateModTest(new ModTestData + { + Autoplay = false, + Mod = new TestAutoMod(), + Beatmap = new Beatmap + { + HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } + }, + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < 0 && Player.Results[0].Type == HitResult.Miss + }); + + private class TestAutoMod : OsuModAutoplay + { + public override Score CreateReplayScore(IBeatmap beatmap) => new Score + { + ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, + Replay = new MissingAutoGenerator(beatmap).Generate() + }; + } + + private class MissingAutoGenerator : OsuAutoGeneratorBase + { + public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap; + + public MissingAutoGenerator(IBeatmap beatmap) + : base(beatmap) + { + } + + public override Replay Generate() + { + AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500))); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500))); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500))); + + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 450, Beatmap.HitObjects[0].StackedPosition)); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 350, Beatmap.HitObjects[0].StackedPosition, OsuAction.LeftButton)); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 325, Beatmap.HitObjects[0].StackedPosition)); + + return Replay; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index a6491bb3f3..6f2998006f 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Scoring new DifficultyRange(HitResult.Great, 80, 50, 20), new DifficultyRange(HitResult.Good, 140, 100, 60), new DifficultyRange(HitResult.Meh, 200, 150, 100), - new DifficultyRange(HitResult.Miss, 200, 200, 200), + new DifficultyRange(HitResult.Miss, 400, 400, 400), }; public override bool IsHitResultAllowed(HitResult result) From 7069cef9ce6a92cfddf6cb8a25d2cb2dbf48e9b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 15:26:39 +0900 Subject: [PATCH 175/387] Add catcher kiai/fail animation states --- .../TestSceneCatcherArea.cs | 58 +++++++++++++++++-- .../CatchSkinComponents.cs | 4 +- .../Skinning/CatchLegacySkinTransformer.cs | 8 +++ .../UI/CatcherAnimationState.cs | 12 ++++ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 36 ++++++++++-- osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 30 ++++++++-- 6 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index df1ac4c725..caaad2f704 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -10,9 +10,15 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests @@ -34,9 +40,41 @@ namespace osu.Game.Rulesets.Catch.Tests CreatedDrawables.OfType().Select(i => i.Child) .OfType().ForEach(c => c.ToggleHyperDash(t))); - AddRepeatStep("catch fruit", () => - this.ChildrenOfType().ForEach(area => - area.MovableCatcher.PlaceOnPlate(new DrawableFruit(new TestSceneFruitObjects.TestCatchFruit(FruitVisualRepresentation.Grape)))), 20); + AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false) + { + X = this.ChildrenOfType().First().MovableCatcher.X + }), 20); + AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false) + { + X = this.ChildrenOfType().First().MovableCatcher.X, + LastInCombo = true, + }), 20); + AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true) + { + X = this.ChildrenOfType().First().MovableCatcher.X, + }), 20); + AddRepeatStep("miss fruit", () => catchFruit(new Fruit + { + X = this.ChildrenOfType().First().MovableCatcher.X + 100, + LastInCombo = true, + }, true), 20); + } + + private void catchFruit(Fruit fruit, bool miss = false) + { + this.ChildrenOfType().ForEach(area => + { + DrawableFruit drawable = new DrawableFruit(fruit); + area.Add(drawable); + + Schedule(() => + { + area.AttemptCatch(fruit); + area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great }); + + drawable.Expire(); + }); + }); } private void createCatcher(float size) @@ -47,7 +85,8 @@ namespace osu.Game.Rulesets.Catch.Tests Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size }) { Anchor = Anchor.CentreLeft, - Origin = Anchor.TopLeft + Origin = Anchor.TopLeft, + CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation }, }); } @@ -58,6 +97,17 @@ namespace osu.Game.Rulesets.Catch.Tests catchRuleset = rulesets.GetRuleset(2); } + public class TestFruit : Fruit + { + public TestFruit(bool kiai) + { + var kiaiCpi = new ControlPointInfo(); + kiaiCpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); + + ApplyDefaultsToSelf(kiaiCpi, new BeatmapDifficulty()); + } + } + private class TestCatcherArea : CatcherArea { public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs index 08bff36401..80390705fe 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Catch FruitOrange, FruitPear, Droplet, - CatcherIdle + CatcherIdle, + CatcherFail, + CatcherKiai } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index af7c60b929..65e6e6f209 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -48,6 +48,14 @@ namespace osu.Game.Rulesets.Catch.Skinning case CatchSkinComponents.CatcherIdle: return this.GetAnimation("fruit-catcher-idle", true, true, true) ?? this.GetAnimation("fruit-ryuuta", true, true, true); + + case CatchSkinComponents.CatcherFail: + return this.GetAnimation("fruit-catcher-fail", true, true, true) ?? + this.GetAnimation("fruit-ryuuta", true, true, true); + + case CatchSkinComponents.CatcherKiai: + return this.GetAnimation("fruit-catcher-kiai", true, true, true) ?? + this.GetAnimation("fruit-ryuuta", true, true, true); } return null; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs new file mode 100644 index 0000000000..566e9d1911 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs @@ -0,0 +1,12 @@ +// 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.Rulesets.Catch.UI +{ + public enum CatcherAnimationState + { + Idle, + Fail, + Kiai + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index dfeaf6e89f..2beda02398 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -155,11 +155,21 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, }, - createCatcherSprite().With(c => - { - c.Anchor = Anchor.TopCentre; - }) }; + + updateCatcher(); + } + + private Drawable catcherSprite; + + private void updateCatcher() + { + catcherSprite?.Expire(); + + Add(catcherSprite = createCatcherSprite().With(c => + { + c.Anchor = Anchor.TopCentre; + })); } private int currentDirection; @@ -222,7 +232,7 @@ namespace osu.Game.Rulesets.Catch.UI Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } - private Drawable createCatcherSprite() => new CatcherSprite(); + private Drawable createCatcherSprite() => new CatcherSprite(currentState); ///

/// Add a caught fruit to the catcher's stack. @@ -290,9 +300,25 @@ namespace osu.Game.Rulesets.Catch.UI SetHyperDashState(); } + if (validCatch) + updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); + else + updateState(CatcherAnimationState.Fail); + return validCatch; } + private void updateState(CatcherAnimationState state) + { + if (currentState == state) + return; + + currentState = state; + updateCatcher(); + } + + private CatcherAnimationState currentState; + private double hyperDashModifier = 1; private int hyperDashDirection; private float hyperDashTargetPosition; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs index 78020114cd..52eb8d597e 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs @@ -14,9 +14,9 @@ namespace osu.Game.Rulesets.Catch.UI { protected override bool ApplySizeRestrictionsToDefault => true; - public CatcherSprite() - : base(new CatchSkinComponent(CatchSkinComponents.CatcherIdle), _ => - new DefaultCatcherSprite(), confineMode: ConfineMode.ScaleDownToFit) + public CatcherSprite(CatcherAnimationState state) + : base(new CatchSkinComponent(componentFromState(state)), _ => + new DefaultCatcherSprite(state), confineMode: ConfineMode.ScaleDownToFit) { RelativeSizeAxes = Axes.None; Size = new Vector2(CatcherArea.CATCHER_SIZE); @@ -25,12 +25,34 @@ namespace osu.Game.Rulesets.Catch.UI OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; } + private static CatchSkinComponents componentFromState(CatcherAnimationState state) + { + switch (state) + { + case CatcherAnimationState.Fail: + return CatchSkinComponents.CatcherFail; + + case CatcherAnimationState.Kiai: + return CatchSkinComponents.CatcherKiai; + + default: + return CatchSkinComponents.CatcherIdle; + } + } + private class DefaultCatcherSprite : Sprite { + private readonly CatcherAnimationState state; + + public DefaultCatcherSprite(CatcherAnimationState state) + { + this.state = state; + } + [BackgroundDependencyLoader] private void load(TextureStore textures) { - Texture = textures.Get("Gameplay/catch/fruit-catcher-idle"); + Texture = textures.Get($"Gameplay/catch/fruit-catcher-{state.ToString().ToLower()}"); } } } From 678f33eea36615c5a2ac2463ff13fc3c6f50a704 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 15:45:21 +0900 Subject: [PATCH 176/387] Add late miss judgements --- ...cs => TestSceneMissHitWindowJudgements.cs} | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) rename osu.Game.Rulesets.Osu.Tests/{TestSceneEarlyMissJudgement.cs => TestSceneMissHitWindowJudgements.cs} (58%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs similarity index 58% rename from osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 27a32aa96e..5f3596976d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Tests.Visual; @@ -16,24 +17,54 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneEarlyMissJudgement : ModTestScene + public class TestSceneMissHitWindowJudgements : ModTestScene { - public TestSceneEarlyMissJudgement() + public TestSceneMissHitWindowJudgements() : base(new OsuRuleset()) { } [Test] - public void TestHitCircleEarly() => CreateModTest(new ModTestData + public void TestMissViaEarlyHit() { - Autoplay = false, - Mod = new TestAutoMod(), - Beatmap = new Beatmap + var beatmap = new Beatmap { HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } - }, - PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < 0 && Player.Results[0].Type == HitResult.Miss - }); + }; + + var hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + CreateModTest(new ModTestData + { + Autoplay = false, + Mod = new TestAutoMod(), + Beatmap = new Beatmap + { + HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } + }, + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + }); + } + + [Test] + public void TestMissViaNotHitting() + { + var beatmap = new Beatmap + { + HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } + }; + + var hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + CreateModTest(new ModTestData + { + Autoplay = false, + Beatmap = beatmap, + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + }); + } private class TestAutoMod : OsuModAutoplay { From 2b33594400dae72c9dc05d428f927353b53fe407 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 15:59:13 +0900 Subject: [PATCH 177/387] Add random rotation and scale factors to osu!catch bananas --- .../TestSceneBananaShower.cs | 2 ++ .../Objects/Drawables/DrawableBanana.cs | 17 +++++++++++++++++ osu.Game/Tests/Visual/ScreenTestScene.cs | 5 ++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs index 20911b8d06..024c4cefb0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs @@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Tests public override IReadOnlyList RequiredTypes => new[] { typeof(BananaShower), + typeof(Banana), typeof(DrawableBananaShower), + typeof(DrawableBanana), typeof(CatchRuleset), typeof(DrawableCatchRuleset), diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index cf7231ebb2..2e7618b8df 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Utils; using osuTK.Graphics; @@ -22,6 +23,22 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables return colour ??= getBananaColour(); } + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + const float end_scale = 0.6f; + const float random_scale_range = 1.6f; + + ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle())) + .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); + + const float random_angle_range = 180; + + ScaleContainer.RotateTo(random_angle_range * (RNG.NextSingle() * 2 - 1)) + .Then().RotateTo(random_angle_range * (RNG.NextSingle() * 2 - 1), HitObject.TimePreempt); + } + private Color4 getBananaColour() { switch (RNG.Next(0, 3)) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index d26aacf2bc..1a6ebed425 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -38,12 +38,11 @@ namespace osu.Game.Tests.Visual private void addExitAllScreensStep() { - AddUntilStep("exit all screens", () => + AddStep("exit all screens", () => { - if (Stack.CurrentScreen == null) return true; + if (Stack.CurrentScreen == null) return; Stack.Exit(); - return false; }); } } From eab544b49f47d2543ff25d80a281a3531f636f4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 16:41:08 +0900 Subject: [PATCH 178/387] Add afterimage glow when entering hyperdash --- .../TestSceneCatcherArea.cs | 5 ---- .../TestSceneHyperDash.cs | 6 ++++ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 30 ++++++++++++++----- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index df1ac4c725..0eea352603 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -22,11 +22,6 @@ namespace osu.Game.Rulesets.Catch.Tests { private RulesetInfo catchRuleset; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CatcherArea), - }; - public TestSceneCatcherArea() { AddSliderStep("CircleSize", 0, 8, 5, createCatcher); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 7a7c3f4103..30df2202f5 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -15,6 +16,11 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneHyperDash : PlayerTestScene { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(CatcherArea), + }; + public TestSceneHyperDash() : base(new CatchRuleset()) { diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index dfeaf6e89f..cc0f41a14f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -205,21 +205,28 @@ namespace osu.Game.Rulesets.Catch.UI if (!Trail) return; + var additive = createAdditiveSprite(HyperDashing); + + additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); + additive.Expire(true); + + Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); + } + + private Drawable createAdditiveSprite(bool hyperDash) + { var additive = createCatcherSprite(); additive.Anchor = Anchor; additive.Scale = Scale; - additive.Colour = HyperDashing ? Color4.Red : Color4.White; + additive.Colour = hyperDash ? Color4.Red : Color4.White; additive.Blending = BlendingParameters.Additive; additive.RelativePositionAxes = RelativePositionAxes; additive.Position = Position; AdditiveTarget.Add(additive); - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); - additive.Expire(true); - - Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); + return additive; } private Drawable createCatcherSprite() => new CatcherSprite(); @@ -311,14 +318,14 @@ namespace osu.Game.Rulesets.Catch.UI { const float hyper_dash_transition_length = 180; - bool previouslyHyperDashing = HyperDashing; + bool wasHyperDashing = HyperDashing; if (modifier <= 1 || X == targetPosition) { hyperDashModifier = 1; hyperDashDirection = 0; - if (previouslyHyperDashing) + if (wasHyperDashing) { this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); @@ -331,11 +338,18 @@ namespace osu.Game.Rulesets.Catch.UI hyperDashDirection = Math.Sign(targetPosition - X); hyperDashTargetPosition = targetPosition; - if (!previouslyHyperDashing) + if (!wasHyperDashing) { this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); Trail = true; + + var hyperDashEndGlow = createAdditiveSprite(true); + + hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.FadeOut(1200); + hyperDashEndGlow.Expire(true); } } } From 8ad44952f8da454f5d8d77172b6615099f86ebe0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 16:47:53 +0900 Subject: [PATCH 179/387] Remove unused usings --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 0eea352603..cf4843c200 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -1,8 +1,6 @@ // 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 System.Linq; using NUnit.Framework; using osu.Framework.Allocation; From 5329b222f6de157989225761aeaf5dd89c60d72f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 17:49:51 +0900 Subject: [PATCH 180/387] Fix hyperdash test having a zero-length juice stream --- .../TestSceneHyperDash.cs | 45 ++++++++++++------- .../Objects/JuiceStream.cs | 4 +- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 7a7c3f4103..5b3a114506 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -8,7 +8,9 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Catch.Tests { @@ -26,9 +28,11 @@ namespace osu.Game.Rulesets.Catch.Tests public void TestHyperDash() { AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); - AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); + AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast. - for (int i = 0; i < 2; i++) + AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0); + + for (int i = 0; i < 3; i++) { AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); @@ -49,39 +53,50 @@ namespace osu.Game.Rulesets.Catch.Tests }; // Should produce a hyper-dash (edge case test) - beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 308 / 512f, NewCombo = true }); - beatmap.HitObjects.Add(new JuiceStream { StartTime = 2008, X = 56 / 512f, }); + beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56 / 512f, NewCombo = true }); + beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308 / 512f, NewCombo = true }); double startTime = 3000; const float left_x = 0.02f; const float right_x = 0.98f; - createObjects(() => new Fruit(), left_x); - createObjects(() => new JuiceStream(), right_x); - createObjects(() => new JuiceStream(), left_x); - createObjects(() => new Fruit(), right_x); - createObjects(() => new Fruit(), left_x); - createObjects(() => new Fruit(), right_x); - createObjects(() => new JuiceStream(), left_x); + createObjects(() => new Fruit { X = left_x }); + createObjects(() => new TestJuiceStream(right_x), 1); + createObjects(() => new TestJuiceStream(left_x), 1); + createObjects(() => new Fruit { X = right_x }); + createObjects(() => new Fruit { X = left_x }); + createObjects(() => new Fruit { X = right_x }); + createObjects(() => new TestJuiceStream(left_x), 1); return beatmap; - void createObjects(Func createObject, float x) + void createObjects(Func createObject, int count = 3) { const float spacing = 140; - for (int i = 0; i < 3; i++) + for (int i = 0; i < count; i++) { var hitObject = createObject(); - hitObject.X = x; hitObject.StartTime = startTime + i * spacing; - beatmap.HitObjects.Add(hitObject); } startTime += 700; } } + + private class TestJuiceStream : JuiceStream + { + public TestJuiceStream(float x) + { + X = x; + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2(x, 0)), + new PathControlPoint(new Vector2(x + 30, 0)), + }); + } + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 642ff0246e..bcc2d9d9bb 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -24,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Objects public int RepeatCount { get; set; } - public double Velocity; - public double TickDistance; + public double Velocity { get; private set; } + public double TickDistance { get; private set; } /// /// The length of one span of this . From 14192c069f8a694dd7fd7b28a6d6785af44ceb6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 18:05:44 +0900 Subject: [PATCH 181/387] Don't play samples on catching a tiny droplet --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 642ff0246e..1e4d8e15db 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Objects { base.CreateNestedHitObjects(); - var tickSamples = Samples.Select(s => new HitSampleInfo + var dropletSamples = Samples.Select(s => new HitSampleInfo { Bank = s.Bank, Name = @"slidertick", @@ -75,7 +75,6 @@ namespace osu.Game.Rulesets.Catch.Objects { AddNested(new TinyDroplet { - Samples = tickSamples, StartTime = t + lastEvent.Value.Time, X = X + Path.PositionAt( lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH, @@ -93,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Objects case SliderEventType.Tick: AddNested(new Droplet { - Samples = tickSamples, + Samples = dropletSamples, StartTime = e.Time, X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, }); From 4daba48a1df153bd543d9251a9060aa163cbcd36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 19:30:31 +0900 Subject: [PATCH 182/387] Stop rotating DrawableCatchHitObjects at the top level --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 0a8e830af9..cad8892283 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables float startRotation = RNG.NextSingle() * 20; double duration = HitObject.TimePreempt + 2000; - this.RotateTo(startRotation).RotateTo(startRotation + 720, duration); + ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 197ad41247..fae5a10d04 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -13,7 +13,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public DrawableFruit(Fruit h) : base(h) { - Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; } [BackgroundDependencyLoader] @@ -21,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { ScaleContainer.Child = new SkinnableDrawable( new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece()); + + ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; } private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) From 9ad519e5a5dd1207f4f6cca8b3cfbae6bb9906fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 19:35:08 +0900 Subject: [PATCH 183/387] Remove fade and custom InitialLifetimeOffset --- .../Objects/Drawables/DrawableCatchHitObject.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 5bfe0515a1..6844be5941 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -91,10 +91,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss); } - protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; - - protected override void UpdateInitialTransforms() => this.FadeInFromZero(200); - protected override void UpdateStateTransforms(ArmedState state) { var endTime = HitObject.GetEndTime(); From 8ec2c35c4f6833a745eecc5ee98fb7c1eca7e422 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 19:34:52 +0900 Subject: [PATCH 184/387] Change origin of nested objects inside JuiceStream to fix visibility issues --- .../Objects/Drawables/DrawableJuiceStream.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index 932464cfd1..7bc016d94f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -14,11 +15,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private readonly Func> createDrawableRepresentation; private readonly Container dropletContainer; + public override Vector2 OriginPosition => base.OriginPosition - new Vector2(0, CatchHitObject.OBJECT_RADIUS); + public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null) : base(s) { this.createDrawableRepresentation = createDrawableRepresentation; - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; X = 0; @@ -27,6 +30,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override void AddNestedHitObject(DrawableHitObject hitObject) { + hitObject.Origin = Anchor.BottomCentre; + base.AddNestedHitObject(hitObject); dropletContainer.Add(hitObject); } From 8294dd0b717a2f6f0071eee12078d899eca10dc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 19:59:49 +0900 Subject: [PATCH 185/387] Fix changing ruleset at song selectnot scrolling the current selection back into view --- osu.Game/Screens/Select/BeatmapCarousel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 1db97af2f0..71744d8b80 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Select root.AddChild(newSet); - applyActiveCriteria(false, false); + applyActiveCriteria(false); //check if we can/need to maintain our current selection. if (previouslySelectedID != null) @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Select { Debug.Assert(bypassFilters); - applyActiveCriteria(false, true); + applyActiveCriteria(false); } return true; @@ -396,7 +396,7 @@ namespace osu.Game.Screens.Select { if (PendingFilter?.Completed == false) { - applyActiveCriteria(false, false); + applyActiveCriteria(false); Update(); } } @@ -406,10 +406,10 @@ namespace osu.Game.Screens.Select if (newCriteria != null) activeCriteria = newCriteria; - applyActiveCriteria(debounce, true); + applyActiveCriteria(debounce); } - private void applyActiveCriteria(bool debounce, bool scroll) + private void applyActiveCriteria(bool debounce) { if (root.Children.Any() != true) return; @@ -419,7 +419,7 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - if (scroll) scrollPositionCache.Invalidate(); + scrollPositionCache.Invalidate(); } PendingFilter?.Cancel(); From ad7cda87357d32be258e39104a5350ee9f6798a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 20:11:06 +0900 Subject: [PATCH 186/387] Fix download failures causing a non-safe drawable change --- osu.Game/Database/IModelDownloader.cs | 2 ++ osu.Game/Online/DownloadTrackingComposite.cs | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/IModelDownloader.cs b/osu.Game/Database/IModelDownloader.cs index 17f1ccab06..99aeb4eacf 100644 --- a/osu.Game/Database/IModelDownloader.cs +++ b/osu.Game/Database/IModelDownloader.cs @@ -15,11 +15,13 @@ namespace osu.Game.Database { /// /// Fired when a download begins. + /// This is NOT run on the update thread and should be scheduled. /// event Action> DownloadBegan; /// /// Fired when a download is interrupted, either due to user cancellation or failure. + /// This is NOT run on the update thread and should be scheduled. /// event Action> DownloadFailed; diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 6e7ef99c6d..0769be2998 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -53,17 +53,17 @@ namespace osu.Game.Online manager.ItemRemoved += itemRemoved; } - private void downloadBegan(ArchiveDownloadRequest request) + private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => { if (request.Model.Equals(Model.Value)) attachDownload(request); - } + }); - private void downloadFailed(ArchiveDownloadRequest request) + private void downloadFailed(ArchiveDownloadRequest request) => Schedule(() => { if (request.Model.Equals(Model.Value)) attachDownload(null); - } + }); private ArchiveDownloadRequest attachedRequest; From 0be423183daad7a77e186e5834bffdb623c4d08d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 00:36:56 +0900 Subject: [PATCH 187/387] Rename data class --- .../Mods/TestSceneCatchModPerfect.cs | 10 +++++----- .../Mods/TestSceneManiaModPerfect.cs | 4 ++-- .../Mods/TestSceneOsuModPerfect.cs | 6 +++--- .../Mods/TestSceneTaikoModPerfect.cs | 6 +++--- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 10 +++++----- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 56d2fe1ee0..47e91e50d4 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -20,11 +20,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestBananaShower(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new BananaShower { StartTime = 1000, EndTime = 3000 }, false), shouldMiss); + public void TestBananaShower(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new BananaShower { StartTime = 1000, EndTime = 3000 }, false), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestFruit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Fruit { StartTime = 1000 }), shouldMiss); + public void TestFruit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Fruit { StartTime = 1000 }), shouldMiss); [TestCase(false)] [TestCase(true)] @@ -40,15 +40,15 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods }) }; - CreateHitObjectTest(new HitObjectTestCase(stream), shouldMiss); + CreateHitObjectTest(new HitObjectTestData(stream), shouldMiss); } // We only care about testing misses, hits are tested via JuiceStream [TestCase(true)] - public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Droplet { StartTime = 1000 }), shouldMiss); + public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss); // We only care about testing misses, hits are tested via JuiceStream [TestCase(true)] - public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new TinyDroplet { StartTime = 1000 }), shouldMiss); + public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index 4e11a302c9..607d42a1bb 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Note { StartTime = 1000 }), shouldMiss); + public void TestNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Note { StartTime = 1000 }), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); + public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index fc2dfa16ec..b03a894085 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestHitCircle(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new HitCircle { StartTime = 1000 }), shouldMiss); + public void TestHitCircle(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HitCircle { StartTime = 1000 }), shouldMiss); [TestCase(false)] [TestCase(true)] @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) }; - CreateHitObjectTest(new HitObjectTestCase(slider), shouldMiss); + CreateHitObjectTest(new HitObjectTestData(slider), shouldMiss); } [TestCase(false)] @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Position = new Vector2(256, 192) }; - CreateHitObjectTest(new HitObjectTestCase(spinner), shouldMiss); + CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss); } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index fd9d01a3db..d3be2cdf0d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -19,15 +19,15 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new CentreHit { StartTime = 1000 }), shouldMiss); + public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new CentreHit { StartTime = 1000 }), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new DrumRoll { StartTime = 1000, EndTime = 3000 }), shouldMiss); + public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new DrumRoll { StartTime = 1000, EndTime = 3000 }), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); + public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); private class TestTaikoRuleset : TaikoRuleset { diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index d6255d2478..798947eb40 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -20,16 +20,16 @@ namespace osu.Game.Tests.Visual this.mod = mod; } - protected void CreateHitObjectTest(HitObjectTestCase testCaseData, bool shouldMiss) => CreateModTest(new ModTestData + protected void CreateHitObjectTest(HitObjectTestData testData, bool shouldMiss) => CreateModTest(new ModTestData { Mod = mod, Beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo }, - HitObjects = { testCaseData.HitObject } + HitObjects = { testData.HitObject } }, Autoplay = !shouldMiss, - PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testCaseData.FailOnMiss) + PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss) }); protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(); @@ -52,12 +52,12 @@ namespace osu.Game.Tests.Visual } } - protected class HitObjectTestCase + protected class HitObjectTestData { public readonly HitObject HitObject; public readonly bool FailOnMiss; - public HitObjectTestCase(HitObject hitObject, bool failOnMiss = true) + public HitObjectTestData(HitObject hitObject, bool failOnMiss = true) { HitObject = hitObject; FailOnMiss = failOnMiss; From 998ca05a0cf144bb30a8b7f7605369e35cac45e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:35:36 +0900 Subject: [PATCH 188/387] Fix disclaimer test scene supporter toggle --- osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs index 681bf1b40b..49fab08ded 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Menus API.LocalUser.Value = new User { Username = API.LocalUser.Value.Username, - Id = API.LocalUser.Value.Id, + Id = API.LocalUser.Value.Id + 1, IsSupporter = !API.LocalUser.Value.IsSupporter, }; }); From 7b368dca359f9ec9a766877cc77d7edbfe821fdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:59:24 +0900 Subject: [PATCH 189/387] Add test coverage --- .../SongSelect/TestScenePlaySongSelect.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 51d302123b..105d96cdfe 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -465,6 +465,43 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmap.OnlineBeatmapID == target.OnlineBeatmapID); } + [Test] + public void TestExternalBeatmapChangeWhileFilteredThenRefilter() + { + createSongSelect(); + addManyTestMaps(); + + changeRuleset(0); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + + AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); + + AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); + + AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmap == null); + + BeatmapInfo target = null; + + AddStep("select beatmap externally", () => + { + target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == 1)) + .ElementAt(5).Beatmaps.First(); + + Beatmap.Value = manager.GetWorkingBeatmap(target); + }); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); + AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); + + AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nononoo"); + + AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap); + AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmap == null); + } + [Test] public void TestAutoplayViaCtrlEnter() { From ed837d311529bfe180871ac4600824daff890a21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 10:18:41 +0900 Subject: [PATCH 190/387] Use framework extension method for FromHex --- .../Objects/Drawables/DrawableSpinner.cs | 4 +- .../Visual/Online/TestSceneChatLink.cs | 3 +- .../Components/TournamentMatchChatDisplay.cs | 2 +- .../Screens/Gameplay/Components/TeamScore.cs | 4 +- .../Ladder/Components/DrawableMatchTeam.cs | 9 +- .../Screens/Ladder/LadderScreen.cs | 4 +- osu.Game.Tournament/TournamentGame.cs | 12 +- osu.Game/Graphics/OsuColour.cs | 214 +++++++----------- .../UserInterface/DrawableOsuMenuItem.cs | 5 +- .../UserInterfaceV2/LabelledDrawable.cs | 3 +- osu.Game/Online/Leaderboards/DrawableRank.cs | 24 +- .../Online/Leaderboards/LeaderboardScore.cs | 8 +- osu.Game/Overlays/BeatmapSet/BasicStats.cs | 5 +- .../BeatmapSet/Buttons/HeaderButton.cs | 8 +- osu.Game/Overlays/Chat/ChatLine.cs | 68 +++--- .../Chat/Selection/ChannelSelectionOverlay.cs | 10 +- .../Chat/Tabs/PrivateChannelTabItem.cs | 2 +- osu.Game/Overlays/Dialog/PopupDialog.cs | 7 +- osu.Game/Overlays/Dialog/PopupDialogButton.cs | 4 +- osu.Game/Overlays/Direct/FilterControl.cs | 3 +- osu.Game/Overlays/Direct/Header.cs | 3 +- osu.Game/Overlays/DirectOverlay.cs | 7 +- osu.Game/Overlays/MedalOverlay.cs | 6 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 +- .../Profile/Header/TopHeaderContainer.cs | 3 +- osu.Game/Overlays/Profile/ProfileHeader.cs | 3 +- osu.Game/Overlays/Social/FilterControl.cs | 4 +- osu.Game/Overlays/Social/Header.cs | 3 +- osu.Game/Overlays/SocialOverlay.cs | 8 +- .../Edit/Components/Menus/EditorMenuBar.cs | 3 +- .../Components/Timeline/TimelineArea.cs | 8 +- osu.Game/Screens/Menu/IntroSequence.cs | 12 +- osu.Game/Screens/Menu/OsuLogo.cs | 8 +- .../Multi/Components/DrawableGameType.cs | 3 +- .../Multi/Components/ParticipantsList.cs | 4 +- osu.Game/Screens/Multi/Header.cs | 3 +- .../Multi/Lounge/Components/DrawableRoom.cs | 2 +- .../Screens/Multi/Match/Components/Footer.cs | 3 +- .../Match/Components/MatchSettingsOverlay.cs | 4 +- .../Match/Components/PurpleTriangleButton.cs | 8 +- .../Components/RoomAvailabilityPicker.cs | 3 +- osu.Game/Screens/Multi/Multiplayer.cs | 11 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 +- .../Carousel/DrawableCarouselBeatmap.cs | 5 +- 44 files changed, 249 insertions(+), 276 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index de11ab6419..0ec7f2ebfe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly SpriteIcon symbol; - private readonly Color4 baseColour = OsuColour.FromHex(@"002c3c"); - private readonly Color4 fillColour = OsuColour.FromHex(@"005b7c"); + private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c"); + private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c"); private readonly IBindable positionBindable = new Bindable(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index a1c77e2db0..c76d4fd5b8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -102,7 +103,7 @@ namespace osu.Game.Tests.Visual.Online { bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour); - Color4 textColour = isAction && hasBackground ? OsuColour.FromHex(newLine.Message.Sender.Colour) : Color4.White; + Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White; var linkCompilers = newLine.ContentFlow.Where(d => d is DrawableLinkCompiler).ToList(); var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts); diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 8eb1c98ba0..2a183d0d45 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tournament.Components // else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id)) // SenderText.Colour = TournamentGame.COLOUR_BLUE; // else if (Message.Sender.Colour != null) - // SenderText.Colour = ColourBox.Colour = OsuColour.FromHex(Message.Sender.Colour); + // SenderText.Colour = ColourBox.Colour = Color4Extensions.FromHex(Message.Sender.Colour); } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index c7071484ca..36c78c5ac1 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components }, box = new Box { - Colour = OsuColour.FromHex("#FFE8AD"), + Colour = Color4Extensions.FromHex("#FFE8AD"), RelativeSizeAxes = Axes.Both, }, }; @@ -85,7 +85,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = OsuColour.FromHex("#FFE8AD").Opacity(0.1f), + Colour = Color4Extensions.FromHex("#FFE8AD").Opacity(0.1f), Hollow = true, Radius = 20, Roundness = 10, diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index fe7e80873c..15cb7e44cb 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -85,8 +86,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.ladderEditor = ladderEditor; colourWinner = losers - ? OsuColour.FromHex("#8E7F48") - : OsuColour.FromHex("#1462AA"); + ? Color4Extensions.FromHex("#8E7F48") + : Color4Extensions.FromHex("#1462AA"); InternalChildren = new Drawable[] { @@ -180,8 +181,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { bool winner = completed.Value && isWinner?.Invoke() == true; - background.FadeColour(winner ? Color4.White : OsuColour.FromHex("#444"), winner ? 500 : 0, Easing.OutQuint); - backgroundRight.FadeColour(winner ? colourWinner : OsuColour.FromHex("#333"), winner ? 500 : 0, Easing.OutQuint); + background.FadeColour(winner ? Color4.White : Color4Extensions.FromHex("#444"), winner ? 500 : 0, Easing.OutQuint); + backgroundRight.FadeColour(winner ? colourWinner : Color4Extensions.FromHex("#333"), winner ? 500 : 0, Easing.OutQuint); AcronymText.Colour = winner ? Color4.Black : Color4.White; diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index c7e59cfa7b..6f62b3ddba 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -32,8 +32,8 @@ namespace osu.Game.Tournament.Screens.Ladder [BackgroundDependencyLoader] private void load(OsuColour colours, Storage storage) { - normalPathColour = OsuColour.FromHex("#66D1FF"); - losersPathColour = OsuColour.FromHex("#FFC700"); + normalPathColour = Color4Extensions.FromHex("#66D1FF"); + losersPathColour = Color4Extensions.FromHex("#FFC700"); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 6d597d5e7d..78bb66d553 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -1,9 +1,9 @@ // 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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; -using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Tournament.Models; using osuTK.Graphics; @@ -14,13 +14,13 @@ namespace osu.Game.Tournament { public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; - public static readonly Color4 COLOUR_RED = OsuColour.FromHex("#AA1414"); - public static readonly Color4 COLOUR_BLUE = OsuColour.FromHex("#1462AA"); + public static readonly Color4 COLOUR_RED = Color4Extensions.FromHex("#AA1414"); + public static readonly Color4 COLOUR_BLUE = Color4Extensions.FromHex("#1462AA"); - public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = OsuColour.FromHex("#fff"); - public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = OsuColour.FromHex("#000"); + public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff"); + public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000"); - public static readonly Color4 TEXT_COLOUR = OsuColour.FromHex("#fff"); + public static readonly Color4 TEXT_COLOUR = Color4Extensions.FromHex("#fff"); protected override void LoadComplete() { diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 59dd823266..984f5e52d1 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -1,8 +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 System; -using System.Globalization; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Beatmaps; using osuTK.Graphics; @@ -13,45 +12,6 @@ namespace osu.Game.Graphics public static Color4 Gray(float amt) => new Color4(amt, amt, amt, 1f); public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255); - public static Color4 FromHex(string hex) - { - var hexSpan = hex[0] == '#' ? hex.AsSpan().Slice(1) : hex.AsSpan(); - - switch (hexSpan.Length) - { - default: - throw new ArgumentException(@"Invalid hex string length!"); - - case 3: - return new Color4( - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(2, 1), NumberStyles.HexNumber) * 17), - 255); - - case 6: - return new Color4( - byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber), - 255); - - case 4: - return new Color4( - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17)); - - case 8: - return new Color4( - byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(6, 2), NumberStyles.HexNumber)); - } - } - public Color4 ForDifficultyRating(DifficultyRating difficulty, bool useLighterColour = false) { switch (difficulty) @@ -78,105 +38,105 @@ namespace osu.Game.Graphics } // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less - public readonly Color4 PurpleLighter = FromHex(@"eeeeff"); - public readonly Color4 PurpleLight = FromHex(@"aa88ff"); - public readonly Color4 PurpleLightAlternative = FromHex(@"cba4da"); - public readonly Color4 Purple = FromHex(@"8866ee"); - public readonly Color4 PurpleDark = FromHex(@"6644cc"); - public readonly Color4 PurpleDarkAlternative = FromHex(@"312436"); - public readonly Color4 PurpleDarker = FromHex(@"441188"); + public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); + public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); + public readonly Color4 PurpleLightAlternative = Color4Extensions.FromHex(@"cba4da"); + public readonly Color4 Purple = Color4Extensions.FromHex(@"8866ee"); + public readonly Color4 PurpleDark = Color4Extensions.FromHex(@"6644cc"); + public readonly Color4 PurpleDarkAlternative = Color4Extensions.FromHex(@"312436"); + public readonly Color4 PurpleDarker = Color4Extensions.FromHex(@"441188"); - public readonly Color4 PinkLighter = FromHex(@"ffddee"); - public readonly Color4 PinkLight = FromHex(@"ff99cc"); - public readonly Color4 Pink = FromHex(@"ff66aa"); - public readonly Color4 PinkDark = FromHex(@"cc5288"); - public readonly Color4 PinkDarker = FromHex(@"bb1177"); + public readonly Color4 PinkLighter = Color4Extensions.FromHex(@"ffddee"); + public readonly Color4 PinkLight = Color4Extensions.FromHex(@"ff99cc"); + public readonly Color4 Pink = Color4Extensions.FromHex(@"ff66aa"); + public readonly Color4 PinkDark = Color4Extensions.FromHex(@"cc5288"); + public readonly Color4 PinkDarker = Color4Extensions.FromHex(@"bb1177"); - public readonly Color4 BlueLighter = FromHex(@"ddffff"); - public readonly Color4 BlueLight = FromHex(@"99eeff"); - public readonly Color4 Blue = FromHex(@"66ccff"); - public readonly Color4 BlueDark = FromHex(@"44aadd"); - public readonly Color4 BlueDarker = FromHex(@"2299bb"); + public readonly Color4 BlueLighter = Color4Extensions.FromHex(@"ddffff"); + public readonly Color4 BlueLight = Color4Extensions.FromHex(@"99eeff"); + public readonly Color4 Blue = Color4Extensions.FromHex(@"66ccff"); + public readonly Color4 BlueDark = Color4Extensions.FromHex(@"44aadd"); + public readonly Color4 BlueDarker = Color4Extensions.FromHex(@"2299bb"); - public readonly Color4 YellowLighter = FromHex(@"ffffdd"); - public readonly Color4 YellowLight = FromHex(@"ffdd55"); - public readonly Color4 Yellow = FromHex(@"ffcc22"); - public readonly Color4 YellowDark = FromHex(@"eeaa00"); - public readonly Color4 YellowDarker = FromHex(@"cc6600"); + public readonly Color4 YellowLighter = Color4Extensions.FromHex(@"ffffdd"); + public readonly Color4 YellowLight = Color4Extensions.FromHex(@"ffdd55"); + public readonly Color4 Yellow = Color4Extensions.FromHex(@"ffcc22"); + public readonly Color4 YellowDark = Color4Extensions.FromHex(@"eeaa00"); + public readonly Color4 YellowDarker = Color4Extensions.FromHex(@"cc6600"); - public readonly Color4 GreenLighter = FromHex(@"eeffcc"); - public readonly Color4 GreenLight = FromHex(@"b3d944"); - public readonly Color4 Green = FromHex(@"88b300"); - public readonly Color4 GreenDark = FromHex(@"668800"); - public readonly Color4 GreenDarker = FromHex(@"445500"); + public readonly Color4 GreenLighter = Color4Extensions.FromHex(@"eeffcc"); + public readonly Color4 GreenLight = Color4Extensions.FromHex(@"b3d944"); + public readonly Color4 Green = Color4Extensions.FromHex(@"88b300"); + public readonly Color4 GreenDark = Color4Extensions.FromHex(@"668800"); + public readonly Color4 GreenDarker = Color4Extensions.FromHex(@"445500"); - public readonly Color4 Sky = FromHex(@"6bb5ff"); - public readonly Color4 GreySkyLighter = FromHex(@"c6e3f4"); - public readonly Color4 GreySkyLight = FromHex(@"8ab3cc"); - public readonly Color4 GreySky = FromHex(@"405461"); - public readonly Color4 GreySkyDark = FromHex(@"303d47"); - public readonly Color4 GreySkyDarker = FromHex(@"21272c"); + public readonly Color4 Sky = Color4Extensions.FromHex(@"6bb5ff"); + public readonly Color4 GreySkyLighter = Color4Extensions.FromHex(@"c6e3f4"); + public readonly Color4 GreySkyLight = Color4Extensions.FromHex(@"8ab3cc"); + public readonly Color4 GreySky = Color4Extensions.FromHex(@"405461"); + public readonly Color4 GreySkyDark = Color4Extensions.FromHex(@"303d47"); + public readonly Color4 GreySkyDarker = Color4Extensions.FromHex(@"21272c"); - public readonly Color4 Seafoam = FromHex(@"05ffa2"); - public readonly Color4 GreySeafoamLighter = FromHex(@"9ebab1"); - public readonly Color4 GreySeafoamLight = FromHex(@"4d7365"); - public readonly Color4 GreySeafoam = FromHex(@"33413c"); - public readonly Color4 GreySeafoamDark = FromHex(@"2c3532"); - public readonly Color4 GreySeafoamDarker = FromHex(@"1e2422"); + public readonly Color4 Seafoam = Color4Extensions.FromHex(@"05ffa2"); + public readonly Color4 GreySeafoamLighter = Color4Extensions.FromHex(@"9ebab1"); + public readonly Color4 GreySeafoamLight = Color4Extensions.FromHex(@"4d7365"); + public readonly Color4 GreySeafoam = Color4Extensions.FromHex(@"33413c"); + public readonly Color4 GreySeafoamDark = Color4Extensions.FromHex(@"2c3532"); + public readonly Color4 GreySeafoamDarker = Color4Extensions.FromHex(@"1e2422"); - public readonly Color4 Cyan = FromHex(@"05f4fd"); - public readonly Color4 GreyCyanLighter = FromHex(@"77b1b3"); - public readonly Color4 GreyCyanLight = FromHex(@"436d6f"); - public readonly Color4 GreyCyan = FromHex(@"293d3e"); - public readonly Color4 GreyCyanDark = FromHex(@"243536"); - public readonly Color4 GreyCyanDarker = FromHex(@"1e2929"); + public readonly Color4 Cyan = Color4Extensions.FromHex(@"05f4fd"); + public readonly Color4 GreyCyanLighter = Color4Extensions.FromHex(@"77b1b3"); + public readonly Color4 GreyCyanLight = Color4Extensions.FromHex(@"436d6f"); + public readonly Color4 GreyCyan = Color4Extensions.FromHex(@"293d3e"); + public readonly Color4 GreyCyanDark = Color4Extensions.FromHex(@"243536"); + public readonly Color4 GreyCyanDarker = Color4Extensions.FromHex(@"1e2929"); - public readonly Color4 Lime = FromHex(@"82ff05"); - public readonly Color4 GreyLimeLighter = FromHex(@"deff87"); - public readonly Color4 GreyLimeLight = FromHex(@"657259"); - public readonly Color4 GreyLime = FromHex(@"3f443a"); - public readonly Color4 GreyLimeDark = FromHex(@"32352e"); - public readonly Color4 GreyLimeDarker = FromHex(@"2e302b"); + public readonly Color4 Lime = Color4Extensions.FromHex(@"82ff05"); + public readonly Color4 GreyLimeLighter = Color4Extensions.FromHex(@"deff87"); + public readonly Color4 GreyLimeLight = Color4Extensions.FromHex(@"657259"); + public readonly Color4 GreyLime = Color4Extensions.FromHex(@"3f443a"); + public readonly Color4 GreyLimeDark = Color4Extensions.FromHex(@"32352e"); + public readonly Color4 GreyLimeDarker = Color4Extensions.FromHex(@"2e302b"); - public readonly Color4 Violet = FromHex(@"bf04ff"); - public readonly Color4 GreyVioletLighter = FromHex(@"ebb8fe"); - public readonly Color4 GreyVioletLight = FromHex(@"685370"); - public readonly Color4 GreyViolet = FromHex(@"46334d"); - public readonly Color4 GreyVioletDark = FromHex(@"2c2230"); - public readonly Color4 GreyVioletDarker = FromHex(@"201823"); + public readonly Color4 Violet = Color4Extensions.FromHex(@"bf04ff"); + public readonly Color4 GreyVioletLighter = Color4Extensions.FromHex(@"ebb8fe"); + public readonly Color4 GreyVioletLight = Color4Extensions.FromHex(@"685370"); + public readonly Color4 GreyViolet = Color4Extensions.FromHex(@"46334d"); + public readonly Color4 GreyVioletDark = Color4Extensions.FromHex(@"2c2230"); + public readonly Color4 GreyVioletDarker = Color4Extensions.FromHex(@"201823"); - public readonly Color4 Carmine = FromHex(@"ff0542"); - public readonly Color4 GreyCarmineLighter = FromHex(@"deaab4"); - public readonly Color4 GreyCarmineLight = FromHex(@"644f53"); - public readonly Color4 GreyCarmine = FromHex(@"342b2d"); - public readonly Color4 GreyCarmineDark = FromHex(@"302a2b"); - public readonly Color4 GreyCarmineDarker = FromHex(@"241d1e"); + public readonly Color4 Carmine = Color4Extensions.FromHex(@"ff0542"); + public readonly Color4 GreyCarmineLighter = Color4Extensions.FromHex(@"deaab4"); + public readonly Color4 GreyCarmineLight = Color4Extensions.FromHex(@"644f53"); + public readonly Color4 GreyCarmine = Color4Extensions.FromHex(@"342b2d"); + public readonly Color4 GreyCarmineDark = Color4Extensions.FromHex(@"302a2b"); + public readonly Color4 GreyCarmineDarker = Color4Extensions.FromHex(@"241d1e"); - public readonly Color4 Gray0 = FromHex(@"000"); - public readonly Color4 Gray1 = FromHex(@"111"); - public readonly Color4 Gray2 = FromHex(@"222"); - public readonly Color4 Gray3 = FromHex(@"333"); - public readonly Color4 Gray4 = FromHex(@"444"); - public readonly Color4 Gray5 = FromHex(@"555"); - public readonly Color4 Gray6 = FromHex(@"666"); - public readonly Color4 Gray7 = FromHex(@"777"); - public readonly Color4 Gray8 = FromHex(@"888"); - public readonly Color4 Gray9 = FromHex(@"999"); - public readonly Color4 GrayA = FromHex(@"aaa"); - public readonly Color4 GrayB = FromHex(@"bbb"); - public readonly Color4 GrayC = FromHex(@"ccc"); - public readonly Color4 GrayD = FromHex(@"ddd"); - public readonly Color4 GrayE = FromHex(@"eee"); - public readonly Color4 GrayF = FromHex(@"fff"); + public readonly Color4 Gray0 = Color4Extensions.FromHex(@"000"); + public readonly Color4 Gray1 = Color4Extensions.FromHex(@"111"); + public readonly Color4 Gray2 = Color4Extensions.FromHex(@"222"); + public readonly Color4 Gray3 = Color4Extensions.FromHex(@"333"); + public readonly Color4 Gray4 = Color4Extensions.FromHex(@"444"); + public readonly Color4 Gray5 = Color4Extensions.FromHex(@"555"); + public readonly Color4 Gray6 = Color4Extensions.FromHex(@"666"); + public readonly Color4 Gray7 = Color4Extensions.FromHex(@"777"); + public readonly Color4 Gray8 = Color4Extensions.FromHex(@"888"); + public readonly Color4 Gray9 = Color4Extensions.FromHex(@"999"); + public readonly Color4 GrayA = Color4Extensions.FromHex(@"aaa"); + public readonly Color4 GrayB = Color4Extensions.FromHex(@"bbb"); + public readonly Color4 GrayC = Color4Extensions.FromHex(@"ccc"); + public readonly Color4 GrayD = Color4Extensions.FromHex(@"ddd"); + public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee"); + public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff"); - public readonly Color4 RedLighter = FromHex(@"ffeded"); - public readonly Color4 RedLight = FromHex(@"ed7787"); - public readonly Color4 Red = FromHex(@"ed1121"); - public readonly Color4 RedDark = FromHex(@"ba0011"); - public readonly Color4 RedDarker = FromHex(@"870000"); + public readonly Color4 RedLighter = Color4Extensions.FromHex(@"ffeded"); + public readonly Color4 RedLight = Color4Extensions.FromHex(@"ed7787"); + public readonly Color4 Red = Color4Extensions.FromHex(@"ed1121"); + public readonly Color4 RedDark = Color4Extensions.FromHex(@"ba0011"); + public readonly Color4 RedDarker = Color4Extensions.FromHex(@"870000"); - public readonly Color4 ChatBlue = FromHex(@"17292e"); + public readonly Color4 ChatBlue = Color4Extensions.FromHex(@"17292e"); - public readonly Color4 ContextMenuGray = FromHex(@"223034"); + public readonly Color4 ContextMenuGray = Color4Extensions.FromHex(@"223034"); } } diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 591ed3df83..a3ca851341 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -38,7 +39,7 @@ namespace osu.Game.Graphics.UserInterface sampleClick = audio.Samples.Get(@"UI/generic-select"); BackgroundColour = Color4.Transparent; - BackgroundColourHover = OsuColour.FromHex(@"172023"); + BackgroundColourHover = Color4Extensions.FromHex(@"172023"); updateTextColour(); } @@ -57,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface break; case MenuItemType.Highlighted: - text.Colour = OsuColour.FromHex(@"ffcc22"); + text.Colour = Color4Extensions.FromHex(@"ffcc22"); break; } } diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs index f44bd72aee..0e995ca73d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -42,7 +43,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("1c2125"), + Colour = Color4Extensions.FromHex("1c2125"), }, new FillFlowContainer { diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 20bda4601f..45b91bbf81 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -80,23 +80,23 @@ namespace osu.Game.Online.Leaderboards { case ScoreRank.XH: case ScoreRank.X: - return OsuColour.FromHex(@"ce1c9d"); + return Color4Extensions.FromHex(@"ce1c9d"); case ScoreRank.SH: case ScoreRank.S: - return OsuColour.FromHex(@"00a8b5"); + return Color4Extensions.FromHex(@"00a8b5"); case ScoreRank.A: - return OsuColour.FromHex(@"7cce14"); + return Color4Extensions.FromHex(@"7cce14"); case ScoreRank.B: - return OsuColour.FromHex(@"e3b130"); + return Color4Extensions.FromHex(@"e3b130"); case ScoreRank.C: - return OsuColour.FromHex(@"f18252"); + return Color4Extensions.FromHex(@"f18252"); default: - return OsuColour.FromHex(@"e95353"); + return Color4Extensions.FromHex(@"e95353"); } } @@ -109,23 +109,23 @@ namespace osu.Game.Online.Leaderboards { case ScoreRank.XH: case ScoreRank.SH: - return ColourInfo.GradientVertical(Color4.White, OsuColour.FromHex("afdff0")); + return ColourInfo.GradientVertical(Color4.White, Color4Extensions.FromHex("afdff0")); case ScoreRank.X: case ScoreRank.S: - return ColourInfo.GradientVertical(OsuColour.FromHex(@"ffe7a8"), OsuColour.FromHex(@"ffb800")); + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"ffe7a8"), Color4Extensions.FromHex(@"ffb800")); case ScoreRank.A: - return OsuColour.FromHex(@"275227"); + return Color4Extensions.FromHex(@"275227"); case ScoreRank.B: - return OsuColour.FromHex(@"553a2b"); + return Color4Extensions.FromHex(@"553a2b"); case ScoreRank.C: - return OsuColour.FromHex(@"473625"); + return Color4Extensions.FromHex(@"473625"); default: - return OsuColour.FromHex(@"512525"); + return Color4Extensions.FromHex(@"512525"); } } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index ba92b993a2..1469f29874 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -200,7 +200,7 @@ namespace osu.Game.Online.Leaderboards scoreLabel = new GlowingSpriteText { TextColour = Color4.White, - GlowColour = OsuColour.FromHex(@"83ccfa"), + GlowColour = Color4Extensions.FromHex(@"83ccfa"), Text = score.TotalScore.ToString(@"N0"), Font = OsuFont.Numeric.With(size: 23), }, @@ -325,7 +325,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.Centre, Size = new Vector2(icon_size), Rotation = 45, - Colour = OsuColour.FromHex(@"3087ac"), + Colour = Color4Extensions.FromHex(@"3087ac"), Icon = FontAwesome.Solid.Square, Shadow = true, }, @@ -334,7 +334,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(icon_size - 6), - Colour = OsuColour.FromHex(@"a4edff"), + Colour = Color4Extensions.FromHex(@"a4edff"), Icon = statistic.Icon, }, }, @@ -344,7 +344,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, TextColour = Color4.White, - GlowColour = OsuColour.FromHex(@"83ccfa"), + GlowColour = Color4Extensions.FromHex(@"83ccfa"), Text = statistic.Value, Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold), }, diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index ba0a62ec2f..a2464bef09 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -124,7 +125,7 @@ namespace osu.Game.Overlays.BeatmapSet Icon = FontAwesome.Solid.Square, Size = new Vector2(12), Rotation = 45, - Colour = OsuColour.FromHex(@"441288"), + Colour = Color4Extensions.FromHex(@"441288"), }, new SpriteIcon { @@ -132,7 +133,7 @@ namespace osu.Game.Overlays.BeatmapSet Origin = Anchor.Centre, Icon = icon, Size = new Vector2(12), - Colour = OsuColour.FromHex(@"f7dd55"), + Colour = Color4Extensions.FromHex(@"f7dd55"), Scale = new Vector2(0.8f), }, value = new OsuSpriteText diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderButton.cs index 6de1d3fca7..99b0b2ed3b 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderButton.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.BeatmapSet.Buttons @@ -19,9 +19,9 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons [BackgroundDependencyLoader] private void load() { - BackgroundColour = OsuColour.FromHex(@"094c5f"); - Triangles.ColourLight = OsuColour.FromHex(@"0f7c9b"); - Triangles.ColourDark = OsuColour.FromHex(@"094c5f"); + BackgroundColour = Color4Extensions.FromHex(@"094c5f"); + Triangles.ColourLight = Color4Extensions.FromHex(@"0f7c9b"); + Triangles.ColourDark = Color4Extensions.FromHex(@"094c5f"); Triangles.TriangleScale = 1.5f; } } diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 8abde8a24f..496986dc56 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.Chat EdgeEffect = new EdgeEffectParameters { Radius = 1, - Colour = OsuColour.FromHex(message.Sender.Colour), + Colour = Color4Extensions.FromHex(message.Sender.Colour), Type = EdgeEffectType.Shadow, }, Padding = new MarginPadding { Left = 3, Right = 3, Bottom = 1, Top = -3 }, @@ -172,7 +172,7 @@ namespace osu.Game.Overlays.Chat t.Font = OsuFont.GetFont(italics: true); if (senderHasBackground) - t.Colour = OsuColour.FromHex(message.Sender.Colour); + t.Colour = Color4Extensions.FromHex(message.Sender.Colour); } t.Font = t.Font.With(size: TextSize); @@ -249,41 +249,41 @@ namespace osu.Game.Overlays.Chat private static readonly Color4[] username_colours = { - OsuColour.FromHex("588c7e"), - OsuColour.FromHex("b2a367"), - OsuColour.FromHex("c98f65"), - OsuColour.FromHex("bc5151"), - OsuColour.FromHex("5c8bd6"), - OsuColour.FromHex("7f6ab7"), - OsuColour.FromHex("a368ad"), - OsuColour.FromHex("aa6880"), + Color4Extensions.FromHex("588c7e"), + Color4Extensions.FromHex("b2a367"), + Color4Extensions.FromHex("c98f65"), + Color4Extensions.FromHex("bc5151"), + Color4Extensions.FromHex("5c8bd6"), + Color4Extensions.FromHex("7f6ab7"), + Color4Extensions.FromHex("a368ad"), + Color4Extensions.FromHex("aa6880"), - OsuColour.FromHex("6fad9b"), - OsuColour.FromHex("f2e394"), - OsuColour.FromHex("f2ae72"), - OsuColour.FromHex("f98f8a"), - OsuColour.FromHex("7daef4"), - OsuColour.FromHex("a691f2"), - OsuColour.FromHex("c894d3"), - OsuColour.FromHex("d895b0"), + Color4Extensions.FromHex("6fad9b"), + Color4Extensions.FromHex("f2e394"), + Color4Extensions.FromHex("f2ae72"), + Color4Extensions.FromHex("f98f8a"), + Color4Extensions.FromHex("7daef4"), + Color4Extensions.FromHex("a691f2"), + Color4Extensions.FromHex("c894d3"), + Color4Extensions.FromHex("d895b0"), - OsuColour.FromHex("53c4a1"), - OsuColour.FromHex("eace5c"), - OsuColour.FromHex("ea8c47"), - OsuColour.FromHex("fc4f4f"), - OsuColour.FromHex("3d94ea"), - OsuColour.FromHex("7760ea"), - OsuColour.FromHex("af52c6"), - OsuColour.FromHex("e25696"), + Color4Extensions.FromHex("53c4a1"), + Color4Extensions.FromHex("eace5c"), + Color4Extensions.FromHex("ea8c47"), + Color4Extensions.FromHex("fc4f4f"), + Color4Extensions.FromHex("3d94ea"), + Color4Extensions.FromHex("7760ea"), + Color4Extensions.FromHex("af52c6"), + Color4Extensions.FromHex("e25696"), - OsuColour.FromHex("677c66"), - OsuColour.FromHex("9b8732"), - OsuColour.FromHex("8c5129"), - OsuColour.FromHex("8c3030"), - OsuColour.FromHex("1f5d91"), - OsuColour.FromHex("4335a5"), - OsuColour.FromHex("812a96"), - OsuColour.FromHex("992861"), + Color4Extensions.FromHex("677c66"), + Color4Extensions.FromHex("9b8732"), + Color4Extensions.FromHex("8c5129"), + Color4Extensions.FromHex("8c3030"), + Color4Extensions.FromHex("1f5d91"), + Color4Extensions.FromHex("4335a5"), + Color4Extensions.FromHex("812a96"), + Color4Extensions.FromHex("992861"), }; } } diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index 25a9a51638..b46ca6b040 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -41,10 +41,10 @@ namespace osu.Game.Overlays.Chat.Selection { RelativeSizeAxes = Axes.X; - Waves.FirstWaveColour = OsuColour.FromHex("353535"); - Waves.SecondWaveColour = OsuColour.FromHex("434343"); - Waves.ThirdWaveColour = OsuColour.FromHex("515151"); - Waves.FourthWaveColour = OsuColour.FromHex("595959"); + Waves.FirstWaveColour = Color4Extensions.FromHex("353535"); + Waves.SecondWaveColour = Color4Extensions.FromHex("434343"); + Waves.ThirdWaveColour = Color4Extensions.FromHex("515151"); + Waves.FourthWaveColour = Color4Extensions.FromHex("595959"); Children = new Drawable[] { @@ -154,7 +154,7 @@ namespace osu.Game.Overlays.Chat.Selection { bg.Colour = colours.Gray3; triangles.ColourDark = colours.Gray3; - triangles.ColourLight = OsuColour.FromHex(@"353535"); + triangles.ColourLight = Color4Extensions.FromHex(@"353535"); headerBg.Colour = colours.Gray2.Opacity(0.75f); } diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 1413b8fe78..5b428a3825 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Chat.Tabs { var user = Value.Users.First(); - BackgroundActive = user.Colour != null ? OsuColour.FromHex(user.Colour) : colours.BlueDark; + BackgroundActive = user.Colour != null ? Color4Extensions.FromHex(user.Colour) : colours.BlueDark; BackgroundInactive = BackgroundActive.Darken(0.5f); } } diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 37db78faa1..02ef900dc5 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osuTK; @@ -114,13 +113,13 @@ namespace osu.Game.Overlays.Dialog new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"221a21"), + Colour = Color4Extensions.FromHex(@"221a21"), }, new Triangles { RelativeSizeAxes = Axes.Both, - ColourLight = OsuColour.FromHex(@"271e26"), - ColourDark = OsuColour.FromHex(@"1e171e"), + ColourLight = Color4Extensions.FromHex(@"271e26"), + ColourDark = Color4Extensions.FromHex(@"1e171e"), TriangleScale = 4, }, }, diff --git a/osu.Game/Overlays/Dialog/PopupDialogButton.cs b/osu.Game/Overlays/Dialog/PopupDialogButton.cs index 75bae25b73..76ee438d6d 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogButton.cs @@ -1,7 +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.Game.Graphics; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Dialog @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.Dialog public PopupDialogButton() { Height = 50; - BackgroundColour = OsuColour.FromHex(@"150e14"); + BackgroundColour = Color4Extensions.FromHex(@"150e14"); TextSize = 18; } } diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs index 70a3ab54fb..e5b2b5cc34 100644 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Online.API.Requests; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Direct { private DirectRulesetSelector rulesetSelector; - protected override Color4 BackgroundColour => OsuColour.FromHex(@"384552"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"384552"); protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked; protected override BeatmapSearchCategory DefaultCategory => BeatmapSearchCategory.Leaderboard; diff --git a/osu.Game/Overlays/Direct/Header.cs b/osu.Game/Overlays/Direct/Header.cs index 80870dcb68..5b3e394a18 100644 --- a/osu.Game/Overlays/Direct/Header.cs +++ b/osu.Game/Overlays/Direct/Header.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using osu.Framework.Extensions.Color4Extensions; using osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Direct { public class Header : SearchableListHeader { - protected override Color4 BackgroundColour => OsuColour.FromHex(@"252f3a"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"252f3a"); protected override DirectTab DefaultTab => DirectTab.Search; protected override Drawable CreateHeaderText() => new OsuSpriteText { Text = @"osu!direct", Font = OsuFont.GetFont(size: 25) }; diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index a6f8b65a0d..61986d1cf0 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -34,9 +35,9 @@ namespace osu.Game.Overlays private readonly OsuSpriteText resultCountsText; private FillFlowContainer panels; - protected override Color4 BackgroundColour => OsuColour.FromHex(@"485e74"); - protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"465b71"); - protected override Color4 TrianglesColourDark => OsuColour.FromHex(@"3f5265"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"485e74"); + protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"465b71"); + protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"3f5265"); protected override SearchableListHeader CreateHeader() => new Header(); protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index aa28b0659d..4425c2f168 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -126,14 +126,14 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"05262f"), + Colour = Color4Extensions.FromHex(@"05262f"), }, new Triangles { RelativeSizeAxes = Axes.Both, TriangleScale = 2, - ColourDark = OsuColour.FromHex(@"04222b"), - ColourLight = OsuColour.FromHex(@"052933"), + ColourDark = Color4Extensions.FromHex(@"04222b"), + ColourLight = Color4Extensions.FromHex(@"052933"), }, innerSpin = new Sprite { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 466c953151..e9b3598625 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -63,10 +63,10 @@ namespace osu.Game.Overlays.Mods public ModSelectOverlay() { - Waves.FirstWaveColour = OsuColour.FromHex(@"19b0e2"); - Waves.SecondWaveColour = OsuColour.FromHex(@"2280a2"); - Waves.ThirdWaveColour = OsuColour.FromHex(@"005774"); - Waves.FourthWaveColour = OsuColour.FromHex(@"003a4e"); + Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2"); + Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2"); + Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774"); + Waves.FourthWaveColour = Color4Extensions.FromHex(@"003a4e"); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 6ed4fc3187..2cc1f6533f 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -170,7 +171,7 @@ namespace osu.Game.Overlays.Profile.Header userCountryText.Text = user?.Country?.FullName ?? "Alien"; supporterTag.SupportLevel = user?.SupportLevel ?? 0; titleText.Text = user?.Title ?? string.Empty; - titleText.Colour = OsuColour.FromHex(user?.Colour ?? "fff"); + titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); userStats.Clear(); diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 3e78423a5a..f7c09e33c1 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Profile.Header; using osu.Game.Users; @@ -48,7 +47,7 @@ namespace osu.Game.Overlays.Profile new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(OsuColour.FromHex("222").Opacity(0.8f), OsuColour.FromHex("222").Opacity(0.2f)) + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("222").Opacity(0.8f), Color4Extensions.FromHex("222").Opacity(0.2f)) }, } }; diff --git a/osu.Game/Overlays/Social/FilterControl.cs b/osu.Game/Overlays/Social/FilterControl.cs index 1c2cb95dfe..93fcc3c401 100644 --- a/osu.Game/Overlays/Social/FilterControl.cs +++ b/osu.Game/Overlays/Social/FilterControl.cs @@ -1,16 +1,16 @@ // 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.Extensions.Color4Extensions; using osuTK.Graphics; using osu.Framework.Graphics; -using osu.Game.Graphics; using osu.Game.Overlays.SearchableList; namespace osu.Game.Overlays.Social { public class FilterControl : SearchableListFilterControl { - protected override Color4 BackgroundColour => OsuColour.FromHex(@"47253a"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"47253a"); protected override SocialSortCriteria DefaultTab => SocialSortCriteria.Rank; protected override SortDirection DefaultCategory => SortDirection.Ascending; diff --git a/osu.Game/Overlays/Social/Header.cs b/osu.Game/Overlays/Social/Header.cs index 22bca9b421..22e0fdcd56 100644 --- a/osu.Game/Overlays/Social/Header.cs +++ b/osu.Game/Overlays/Social/Header.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Framework.Allocation; using System.ComponentModel; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Social @@ -17,7 +18,7 @@ namespace osu.Game.Overlays.Social { private OsuSpriteText browser; - protected override Color4 BackgroundColour => OsuColour.FromHex(@"38202e"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"38202e"); protected override SocialTab DefaultTab => SocialTab.AllPlayers; protected override IconUsage Icon => FontAwesome.Solid.Users; diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 54c978738d..50c05e1b54 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -9,7 +9,6 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -18,6 +17,7 @@ using osu.Game.Overlays.Social; using osu.Game.Users; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Threading; namespace osu.Game.Overlays @@ -27,9 +27,9 @@ namespace osu.Game.Overlays private readonly LoadingSpinner loading; private FillFlowContainer panels; - protected override Color4 BackgroundColour => OsuColour.FromHex(@"60284b"); - protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"672b51"); - protected override Color4 TrianglesColourDark => OsuColour.FromHex(@"5c2648"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"60284b"); + protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"672b51"); + protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"5c2648"); protected override SearchableListHeader CreateHeader() => new Header(); protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 752615245e..afd9e3d760 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -26,7 +27,7 @@ namespace osu.Game.Screens.Edit.Components.Menus MaskingContainer.CornerRadius = 0; ItemsContainer.Padding = new MarginPadding { Left = 100 }; - BackgroundColour = OsuColour.FromHex("111"); + BackgroundColour = Color4Extensions.FromHex("111"); ScreenSelectionTabControl tabControl; AddRangeInternal(new Drawable[] diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 02e5db306d..b99a053859 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osuTK; @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("111") + Colour = Color4Extensions.FromHex("111") }, new GridContainer { @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("222") + Colour = Color4Extensions.FromHex("222") }, new FillFlowContainer { @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("333") + Colour = Color4Extensions.FromHex("333") }, new Container { diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs index e2dd14b18c..6731fef6f7 100644 --- a/osu.Game/Screens/Menu/IntroSequence.cs +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -94,7 +94,7 @@ namespace osu.Game.Screens.Menu }, } }, - bigRing = new Ring(OsuColour.FromHex(@"B6C5E9"), 0.85f), + bigRing = new Ring(Color4Extensions.FromHex(@"B6C5E9"), 0.85f), mediumRing = new Ring(Color4.White.Opacity(130), 0.7f), smallRing = new Ring(Color4.White, 0.6f), welcomeText = new OsuSpriteText @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Height = 0, - Colour = OsuColour.FromHex(@"C6D8FF").Opacity(160), + Colour = Color4Extensions.FromHex(@"C6D8FF").Opacity(160), }, foregroundFill = new Box { @@ -139,28 +139,28 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.TopCentre, Position = new Vector2(0, circle_offset), - Colour = OsuColour.FromHex(@"AA92FF"), + Colour = Color4Extensions.FromHex(@"AA92FF"), }, blueCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.CentreRight, Position = new Vector2(-circle_offset, 0), - Colour = OsuColour.FromHex(@"8FE5FE"), + Colour = Color4Extensions.FromHex(@"8FE5FE"), }, yellowCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.BottomCentre, Position = new Vector2(0, -circle_offset), - Colour = OsuColour.FromHex(@"FFD64C"), + Colour = Color4Extensions.FromHex(@"FFD64C"), }, pinkCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, Position = new Vector2(circle_offset, 0), - Colour = OsuColour.FromHex(@"e967a1"), + Colour = Color4Extensions.FromHex(@"e967a1"), }, }; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index be2f29cbe9..800520100e 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,7 +15,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osuTK; @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Menu /// public class OsuLogo : BeatSyncedContainer { - public readonly Color4 OsuPink = OsuColour.FromHex(@"e967a1"); + public readonly Color4 OsuPink = Color4Extensions.FromHex(@"e967a1"); private const double transition_length = 300; @@ -176,8 +176,8 @@ namespace osu.Game.Screens.Menu triangles = new Triangles { TriangleScale = 4, - ColourLight = OsuColour.FromHex(@"ff7db7"), - ColourDark = OsuColour.FromHex(@"de5b95"), + ColourLight = Color4Extensions.FromHex(@"ff7db7"), + ColourDark = Color4Extensions.FromHex(@"de5b95"), RelativeSizeAxes = Axes.Both, }, } diff --git a/osu.Game/Screens/Multi/Components/DrawableGameType.cs b/osu.Game/Screens/Multi/Components/DrawableGameType.cs index f4941dd73a..28240f0796 100644 --- a/osu.Game/Screens/Multi/Components/DrawableGameType.cs +++ b/osu.Game/Screens/Multi/Components/DrawableGameType.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -27,7 +28,7 @@ namespace osu.Game.Screens.Multi.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"545454"), + Colour = Color4Extensions.FromHex(@"545454"), }, }; } diff --git a/osu.Game/Screens/Multi/Components/ParticipantsList.cs b/osu.Game/Screens/Multi/Components/ParticipantsList.cs index 5a2dc19b66..79d130adf5 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantsList.cs @@ -2,12 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; -using osu.Game.Graphics; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Multi.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"27252d"), + Colour = Color4Extensions.FromHex(@"27252d"), }, avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }, }; diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 1cbf2a45e7..0a05472ba3 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.Multi new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"2f2043"), + Colour = Color4Extensions.FromHex(@"2f2043"), }, new Container { diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index d45dac1ae6..de02d779e1 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"212121"), + Colour = Color4Extensions.FromHex(@"212121"), }, new StatusColouredContainer(transition_duration) { diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index c0c866d815..94d7df6194 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -44,7 +45,7 @@ namespace osu.Game.Screens.Multi.Match.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { - background.Colour = OsuColour.FromHex(@"28242d"); + background.Colour = Color4Extensions.FromHex(@"28242d"); } } } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 115ac5037a..5d68de9ce6 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Multi.Match.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"28242d"), + Colour = Color4Extensions.FromHex(@"28242d"), }, new GridContainer { @@ -270,7 +270,7 @@ namespace osu.Game.Screens.Multi.Match.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), }, new FillFlowContainer { diff --git a/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs b/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs index 8a0369ceba..1d93116d07 100644 --- a/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Multi.Match.Components @@ -12,9 +12,9 @@ namespace osu.Game.Screens.Multi.Match.Components [BackgroundDependencyLoader] private void load() { - BackgroundColour = OsuColour.FromHex(@"593790"); - Triangles.ColourLight = OsuColour.FromHex(@"7247b6"); - Triangles.ColourDark = OsuColour.FromHex(@"593790"); + BackgroundColour = Color4Extensions.FromHex(@"593790"); + Triangles.ColourLight = Color4Extensions.FromHex(@"7247b6"); + Triangles.ColourDark = Color4Extensions.FromHex(@"593790"); } } } diff --git a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs b/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs index 9de4a61cde..7ef39c2a74 100644 --- a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs +++ b/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; @@ -52,7 +53,7 @@ namespace osu.Game.Screens.Multi.Match.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"3d3943"), + Colour = Color4Extensions.FromHex(@"3d3943"), }, selection = new Box { diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index b0d773869a..863a28609b 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -13,7 +13,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -75,7 +74,7 @@ namespace osu.Game.Screens.Multi RelativeSizeAxes = Axes.Both; Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; - var backgroundColour = OsuColour.FromHex(@"3e3a44"); + var backgroundColour = Color4Extensions.FromHex(@"3e3a44"); InternalChild = waves = new MultiplayerWaveContainer { @@ -386,10 +385,10 @@ namespace osu.Game.Screens.Multi public MultiplayerWaveContainer() { - FirstWaveColour = OsuColour.FromHex(@"654d8c"); - SecondWaveColour = OsuColour.FromHex(@"554075"); - ThirdWaveColour = OsuColour.FromHex(@"44325e"); - FourthWaveColour = OsuColour.FromHex(@"392850"); + FirstWaveColour = Color4Extensions.FromHex(@"654d8c"); + SecondWaveColour = Color4Extensions.FromHex(@"554075"); + ThirdWaveColour = Color4Extensions.FromHex(@"44325e"); + FourthWaveColour = Color4Extensions.FromHex(@"392850"); } } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index cf49cf0228..f84aac3081 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -384,7 +384,7 @@ namespace osu.Game.Screens.Select Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"441288"), + Colour = Color4Extensions.FromHex(@"441288"), Icon = FontAwesome.Solid.Square, Rotation = 45, }, @@ -394,7 +394,7 @@ namespace osu.Game.Screens.Select Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Scale = new Vector2(0.8f), - Colour = OsuColour.FromHex(@"f7dd55"), + Colour = Color4Extensions.FromHex(@"f7dd55"), Icon = statistic.Icon, }, } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 50419a5fb9..841bbf415c 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -69,8 +70,8 @@ namespace osu.Game.Screens.Select.Carousel { TriangleScale = 2, RelativeSizeAxes = Axes.Both, - ColourLight = OsuColour.FromHex(@"3a7285"), - ColourDark = OsuColour.FromHex(@"123744") + ColourLight = Color4Extensions.FromHex(@"3a7285"), + ColourDark = Color4Extensions.FromHex(@"123744") }, new FillFlowContainer { From 9683c0756d391930d6514e4d3a93ba9021ad623a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 12:56:12 +0900 Subject: [PATCH 191/387] Start path at (0,0) --- osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 5b3a114506..f39395ba44 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -91,10 +91,11 @@ namespace osu.Game.Rulesets.Catch.Tests public TestJuiceStream(float x) { X = x; + Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(x, 0)), - new PathControlPoint(new Vector2(x + 30, 0)), + new PathControlPoint(Vector2.Zero), + new PathControlPoint(new Vector2(30, 0)), }); } } From 638a9a24aa20d9c028dd750ae59b6075f781e93c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:35:49 +0900 Subject: [PATCH 192/387] Initial disclaimer updates --- osu.Game/Screens/Menu/Disclaimer.cs | 89 ++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index bcab73715b..2c1b0c3166 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -33,6 +34,7 @@ namespace osu.Game.Screens.Menu private readonly OsuScreen nextScreen; private readonly Bindable currentUser = new Bindable(); + private FillFlowContainer fill; public Disclaimer(OsuScreen nextScreen = null) { @@ -49,16 +51,16 @@ namespace osu.Game.Screens.Menu { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.ExclamationTriangle, + Icon = FontAwesome.Solid.Poo, Size = new Vector2(icon_size), Y = icon_y, }, - new FillFlowContainer + fill = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Y = icon_y + icon_size, + Y = icon_y, Anchor = Anchor.Centre, Origin = Anchor.TopCentre, Children = new Drawable[] @@ -71,6 +73,8 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Spacing = new Vector2(0, 2), + LayoutDuration = 2000, + LayoutEasing = Easing.OutQuint }, supportFlow = new LinkFlowContainer { @@ -86,23 +90,16 @@ namespace osu.Game.Screens.Menu } }; - textFlow.AddText("This is an ", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.Light)); - textFlow.AddText("early development build", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.SemiBold)); + textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.Light)); + textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.SemiBold)); - textFlow.AddParagraph("Things may not work as expected", t => t.Font = t.Font.With(size: 20)); textFlow.NewParagraph(); static void format(SpriteText t) => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold); - textFlow.AddParagraph("Detailed bug reports are welcomed via github issues.", format); + textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Exo, 20, FontWeight.SemiBold)); textFlow.NewParagraph(); - textFlow.AddText("Visit ", format); - textFlow.AddLink("discord.gg/ppy", "https://discord.gg/ppy", creationParameters: format); - textFlow.AddText(" to help out or follow progress!", format); - - textFlow.NewParagraph(); - textFlow.NewParagraph(); textFlow.NewParagraph(); iconColour = colours.Yellow; @@ -114,7 +111,7 @@ namespace osu.Game.Screens.Menu if (e.NewValue.IsSupporter) { - supportFlow.AddText("Thank you for supporting osu!", format); + supportFlow.AddText("Eternal thanks to you for supporting osu!", format); } else { @@ -125,7 +122,7 @@ namespace osu.Game.Screens.Menu heart = supportFlow.AddIcon(FontAwesome.Solid.Heart, t => { - t.Padding = new MarginPadding { Left = 5 }; + t.Padding = new MarginPadding { Left = 5, Top = 3 }; t.Font = t.Font.With(size: 12); t.Origin = Anchor.Centre; t.Colour = colours.Pink; @@ -139,11 +136,6 @@ namespace osu.Game.Screens.Menu }, true); } - private void animateHeart() - { - heart.FlashColour(Color4.White, 750, Easing.OutQuint).Loop(); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -155,15 +147,28 @@ namespace osu.Game.Screens.Menu { base.OnEntering(last); - icon.Delay(1000).FadeColour(iconColour, 200, Easing.OutQuint); - icon.Delay(1000) - .MoveToY(icon_y * 1.1f, 160, Easing.OutCirc) - .RotateTo(-10, 160, Easing.OutCirc) - .Then() - .MoveToY(icon_y, 160, Easing.InCirc) - .RotateTo(0, 160, Easing.InCirc); + icon.RotateTo(10); + icon.FadeOut(); + icon.ScaleTo(0.5f); + + icon.Delay(500).FadeIn(500).ScaleTo(1, 500, Easing.OutQuint); + + using (BeginDelayedSequence(3000, true)) + { + icon.FadeColour(iconColour, 200, Easing.OutQuint); + icon.MoveToY(icon_y * 1.3f, 500, Easing.OutCirc) + .RotateTo(-360, 520, Easing.OutQuint) + .Then() + .MoveToY(icon_y, 160, Easing.InQuart) + .FadeColour(Color4.White, 160); + + fill.Delay(520 + 160).MoveToOffset(new Vector2(0, 15), 160, Easing.OutQuart); + } supportFlow.FadeOut().Delay(2000).FadeIn(500); + double delay = 500; + foreach (var c in textFlow.Children) + c.FadeTo(0.001f).Delay(delay += 20).FadeIn(500); animateHeart(); @@ -178,5 +183,35 @@ namespace osu.Game.Screens.Menu this.Push(nextScreen); }); } + + private string getRandomTip() + { + string[] tips = + { + "You can press Ctrl-T anywhere in the game to toggle the toolbar!", + "You can press Ctrl-O anywhere in the game to access options!", + "All settings are dynamic and take effect in real-time. Try changing the skin while playing!", + "New features are coming online every update. Make sure to stay up-to-date!", + "If you find the UI too large or small, try adjusting UI scale in settings!", + "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", + "For now, osu!direct is available to all users on lazer. You can access it anywhere using Ctrl-D!", + "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!", + "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", + "Try scrolling down in the mod select panel to find a bunch of new fun mods!", + "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", + "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", + "All delete operations are temoprary until quit. Restore accidentally deleted content from the maintenance settings!", + "Check out the \"timeshift\" multiplayer system, which has local permanent leaderboards and playlist support!", + "Toggle advanced frame / thread statistics with Ctrl-F11!", + "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", + }; + + return tips[RNG.Next(0, tips.Length)]; + } + + private void animateHeart() + { + heart.FlashColour(Color4.White, 750, Easing.OutQuint).Loop(); + } } } From b8d3e644166381e5f3d3b3e52a4c213dd939253c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:49:20 +0900 Subject: [PATCH 193/387] Rename loader test scene --- .../Menus/{TestSceneLoaderAnimation.cs => TestSceneLoader.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/Menus/{TestSceneLoaderAnimation.cs => TestSceneLoader.cs} (96%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs similarity index 96% rename from osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs rename to osu.Game.Tests/Visual/Menus/TestSceneLoader.cs index 61fed3013e..82b0139155 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs @@ -14,14 +14,14 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Menus { [TestFixture] - public class TestSceneLoaderAnimation : ScreenTestScene + public class TestSceneLoader : ScreenTestScene { private TestLoader loader; [Cached] private OsuLogo logo; - public TestSceneLoaderAnimation() + public TestSceneLoader() { Child = logo = new OsuLogo { From 4012e878b06dedfe739a6725cb30b7b515bad78c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:49:34 +0900 Subject: [PATCH 194/387] Update loader look --- .../Graphics/UserInterface/LoadingSpinner.cs | 6 ++- osu.Game/Screens/Loader.cs | 39 ++++++++----------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index 36d429b8c1..fed3dda579 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -27,7 +27,8 @@ namespace osu.Game.Graphics.UserInterface /// Constuct a new loading spinner. /// /// Whether the spinner should have a surrounding black box for visibility. - public LoadingSpinner(bool withBox = false) + /// Whether colours should be inverted (black spinner instead of white). + public LoadingSpinner(bool withBox = false, bool inverted = false) { Size = new Vector2(60); @@ -45,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface { new Box { - Colour = Color4.Black, + Colour = inverted ? Color4.White : Color4.Black, RelativeSizeAxes = Axes.Both, Alpha = withBox ? 0.7f : 0 }, @@ -53,6 +54,7 @@ namespace osu.Game.Graphics.UserInterface { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Colour = inverted ? Color4.Black : Color4.White, Scale = new Vector2(withBox ? 0.6f : 1), RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.CircleNotch diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 289413c65a..d26dc0d660 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -8,9 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shaders; using osu.Framework.Utils; using osu.Game.Screens.Menu; -using osuTK; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Screens @@ -24,31 +25,12 @@ namespace osu.Game.Screens ValidForResume = false; } - protected override void LogoArriving(OsuLogo logo, bool resuming) - { - base.LogoArriving(logo, resuming); - - logo.BeatMatching = false; - logo.Triangles = false; - logo.RelativePositionAxes = Axes.None; - logo.Origin = Anchor.BottomRight; - logo.Anchor = Anchor.BottomRight; - logo.Position = new Vector2(-40); - logo.Scale = new Vector2(0.2f); - - logo.Delay(500).FadeInFromZero(1000, Easing.OutQuint); - } - - protected override void LogoSuspending(OsuLogo logo) - { - base.LogoSuspending(logo); - logo.FadeOut(logo.Alpha * 400); - } - private OsuScreen loadableScreen; private ShaderPrecompiler precompiler; private IntroSequence introSequence; + private LoadingSpinner spinner; + private ScheduledDelegate spinnerShow; protected virtual OsuScreen CreateLoadableScreen() { @@ -82,6 +64,17 @@ namespace osu.Game.Screens LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal); LoadComponentAsync(loadableScreen = CreateLoadableScreen()); + LoadComponentAsync(spinner = new LoadingSpinner(true, true) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding(40), + }, _ => + { + AddInternal(spinner); + spinnerShow = Scheduler.AddDelayed(spinner.Show, 200); + }); + checkIfLoaded(); } @@ -93,6 +86,8 @@ namespace osu.Game.Screens return; } + spinnerShow?.Cancel(); + spinner.Hide(); this.Push(loadableScreen); } From ec88f7a71250bb5acf50273fadcdbb3da1bd3ee7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 13:20:31 +0900 Subject: [PATCH 195/387] Update tests and delay push animation until loader is done disappearing --- .../Visual/Menus/TestSceneLoader.cs | 23 +++++++++++-------- .../Graphics/UserInterface/LoadingSpinner.cs | 2 +- osu.Game/Screens/Loader.cs | 11 +++++++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs index 82b0139155..6003d05ecd 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs @@ -1,12 +1,15 @@ // 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 System.Threading; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK.Graphics; @@ -42,33 +45,33 @@ namespace osu.Game.Tests.Visual.Menus LoadScreen(loader); }); + + AddAssert("spinner did not display", () => loader.LoadingSpinner.Alpha == 0); + + AddUntilStep("loaded", () => loader.ScreenLoaded); + AddUntilStep("not current", () => !loader.IsCurrentScreen()); } [Test] public void TestDelayedLoad() { AddStep("begin loading", () => LoadScreen(loader = new TestLoader())); - AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0); + AddUntilStep("wait for spinner visible", () => loader.LoadingSpinner?.Alpha > 0); AddStep("finish loading", () => loader.AllowLoad.Set()); - AddUntilStep("loaded", () => loader.Logo != null && loader.ScreenLoaded); - AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0); + AddUntilStep("spinner gone", () => loader.LoadingSpinner?.Alpha == 0); + AddUntilStep("loaded", () => loader.ScreenLoaded); + AddUntilStep("not current", () => !loader.IsCurrentScreen()); } private class TestLoader : Loader { public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(); - public OsuLogo Logo; + public LoadingSpinner LoadingSpinner => this.ChildrenOfType().Single(); private TestScreen screen; public bool ScreenLoaded => screen.IsCurrentScreen(); - protected override void LogoArriving(OsuLogo logo, bool resuming) - { - Logo = logo; - base.LogoArriving(logo, resuming); - } - protected override OsuScreen CreateLoadableScreen() => screen = new TestScreen(); protected override ShaderPrecompiler CreateShaderPrecompiler() => new TestShaderPrecompiler(AllowLoad); diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index fed3dda579..4f4607c114 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -19,7 +19,7 @@ namespace osu.Game.Graphics.UserInterface protected Container MainContents; - protected const float TRANSITION_DURATION = 500; + public const float TRANSITION_DURATION = 500; private const float spin_duration = 900; diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index d26dc0d660..a5b55a24e5 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shaders; using osu.Framework.Utils; using osu.Game.Screens.Menu; @@ -87,8 +88,14 @@ namespace osu.Game.Screens } spinnerShow?.Cancel(); - spinner.Hide(); - this.Push(loadableScreen); + + if (spinner.State.Value == Visibility.Visible) + { + spinner.Hide(); + Scheduler.AddDelayed(() => this.Push(loadableScreen), LoadingSpinner.TRANSITION_DURATION); + } + else + this.Push(loadableScreen); } [BackgroundDependencyLoader] From 93aec4e6920759e76c00f10ad322b3fd6ff8cf27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 13:23:26 +0900 Subject: [PATCH 196/387] Improve english Co-Authored-By: Dan Balasescu --- osu.Game/Screens/Menu/Disclaimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 2c1b0c3166..ee8200321b 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -200,7 +200,7 @@ namespace osu.Game.Screens.Menu "Try scrolling down in the mod select panel to find a bunch of new fun mods!", "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", - "All delete operations are temoprary until quit. Restore accidentally deleted content from the maintenance settings!", + "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", "Check out the \"timeshift\" multiplayer system, which has local permanent leaderboards and playlist support!", "Toggle advanced frame / thread statistics with Ctrl-F11!", "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", From 1bad2ff879ca53ecf35bbd5c42faa19c13450454 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 13:45:55 +0900 Subject: [PATCH 197/387] Load all catcher states ahead-of-time to avoid blocking loads --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 47 ++++++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 2beda02398..dca3fea0d1 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Input.Bindings; @@ -148,28 +149,62 @@ namespace osu.Game.Rulesets.Catch.UI [BackgroundDependencyLoader] private void load() { - Children = new[] + Children = new Drawable[] { caughtFruit = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, }, + catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherFail = new CatcherSprite(CatcherAnimationState.Fail) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + } }; updateCatcher(); } - private Drawable catcherSprite; + private CatcherSprite catcherIdle; + private CatcherSprite catcherKiai; + private CatcherSprite catcherFail; private void updateCatcher() { - catcherSprite?.Expire(); + catcherIdle.Hide(); + catcherKiai.Hide(); + catcherFail.Hide(); - Add(catcherSprite = createCatcherSprite().With(c => + CatcherSprite current; + + switch (currentState) { - c.Anchor = Anchor.TopCentre; - })); + default: + current = catcherIdle; + break; + + case CatcherAnimationState.Fail: + current = catcherFail; + break; + + case CatcherAnimationState.Kiai: + current = catcherKiai; + break; + } + + current.Show(); + (current.Drawable as IAnimation)?.GotoFrame(0); } private int currentDirection; From 73b225ad62148567cf8d6148b39706ab7abe0475 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 14:28:13 +0900 Subject: [PATCH 198/387] Make catcher's trail reflect the current animation frame rather than play the full animation --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 52 ++++++++++--------- .../UI/CatcherTrailSprite.cs | 22 ++++++++ 2 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index dca3fea0d1..43d98dc617 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -9,6 +9,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -180,31 +182,29 @@ namespace osu.Game.Rulesets.Catch.UI private CatcherSprite catcherKiai; private CatcherSprite catcherFail; + private CatcherSprite currentCatcher; + private void updateCatcher() { - catcherIdle.Hide(); - catcherKiai.Hide(); - catcherFail.Hide(); - - CatcherSprite current; + currentCatcher?.Hide(); switch (currentState) { default: - current = catcherIdle; + currentCatcher = catcherIdle; break; case CatcherAnimationState.Fail: - current = catcherFail; + currentCatcher = catcherFail; break; case CatcherAnimationState.Kiai: - current = catcherKiai; + currentCatcher = catcherKiai; break; } - current.Show(); - (current.Drawable as IAnimation)?.GotoFrame(0); + currentCatcher.Show(); + (currentCatcher.Drawable as IAnimation)?.GotoFrame(0); } private int currentDirection; @@ -227,14 +227,14 @@ namespace osu.Game.Rulesets.Catch.UI private bool trail; /// - /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// Activate or deactivate the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. /// protected bool Trail { get => trail; set { - if (value == trail) return; + if (value == trail || AdditiveTarget == null) return; trail = value; @@ -245,21 +245,25 @@ namespace osu.Game.Rulesets.Catch.UI private void beginTrail() { - Trail &= dashing || HyperDashing; - Trail &= AdditiveTarget != null; + if (!dashing && !HyperDashing) + { + Trail = false; + return; + } - if (!Trail) return; + Texture tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; - var additive = createCatcherSprite(); + var additive = new CatcherTrailSprite(tex) + { + Anchor = Anchor, + Scale = Scale, + Colour = HyperDashing ? Color4.Red : Color4.White, + Blending = BlendingParameters.Additive, + RelativePositionAxes = RelativePositionAxes, + Position = Position + }; - additive.Anchor = Anchor; - additive.Scale = Scale; - additive.Colour = HyperDashing ? Color4.Red : Color4.White; - additive.Blending = BlendingParameters.Additive; - additive.RelativePositionAxes = RelativePositionAxes; - additive.Position = Position; - - AdditiveTarget.Add(additive); + AdditiveTarget?.Add(additive); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); additive.Expire(true); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs new file mode 100644 index 0000000000..56cb7dbfda --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs @@ -0,0 +1,22 @@ +// 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.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class CatcherTrailSprite : Sprite + { + public CatcherTrailSprite(Texture texture) + { + Texture = texture; + + Size = new Vector2(CatcherArea.CATCHER_SIZE); + + // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. + OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; + } + } +} From 401429feeccbcb92818cfd31f5020ffdf3070abe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 15:52:18 +0900 Subject: [PATCH 199/387] Revert changes to ScrenTestScene --- osu.Game/Tests/Visual/ScreenTestScene.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 1a6ebed425..d26aacf2bc 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -38,11 +38,12 @@ namespace osu.Game.Tests.Visual private void addExitAllScreensStep() { - AddStep("exit all screens", () => + AddUntilStep("exit all screens", () => { - if (Stack.CurrentScreen == null) return; + if (Stack.CurrentScreen == null) return true; Stack.Exit(); + return false; }); } } From 966e5bbc8aa441cecde5fa54fce6f659015b6392 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 15:54:03 +0900 Subject: [PATCH 200/387] User helper function to reduce copy paste --- .../Objects/Drawables/DrawableBanana.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 2e7618b8df..01b76ceed9 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -33,10 +33,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle())) .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); - const float random_angle_range = 180; + ScaleContainer.RotateTo(getRandomAngle()) + .Then() + .RotateTo(getRandomAngle(), HitObject.TimePreempt); - ScaleContainer.RotateTo(random_angle_range * (RNG.NextSingle() * 2 - 1)) - .Then().RotateTo(random_angle_range * (RNG.NextSingle() * 2 - 1), HitObject.TimePreempt); + float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1); } private Color4 getBananaColour() From 6546fd3f812d99141813914df402b6d0ee72ff46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 16:07:44 +0900 Subject: [PATCH 201/387] Fix potential null due to async load --- osu.Game.Tests/Visual/Menus/TestSceneLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs index 6003d05ecd..b3064ba9be 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Menus LoadScreen(loader); }); - AddAssert("spinner did not display", () => loader.LoadingSpinner.Alpha == 0); + AddAssert("spinner did not display", () => loader.LoadingSpinner?.Alpha == 0); AddUntilStep("loaded", () => loader.ScreenLoaded); AddUntilStep("not current", () => !loader.IsCurrentScreen()); @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Menus { public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(); - public LoadingSpinner LoadingSpinner => this.ChildrenOfType().Single(); + public LoadingSpinner LoadingSpinner => this.ChildrenOfType().FirstOrDefault(); private TestScreen screen; public bool ScreenLoaded => screen.IsCurrentScreen(); From 8eb8572c738b549be8915ea809a7ad20d600e307 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:00:39 +0900 Subject: [PATCH 202/387] Apply osu!-side video sprite changes --- osu.Game.Tournament/Components/TourneyVideo.cs | 2 +- osu.Game/Screens/Menu/IntroTriangles.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 43088d6b92..81620b017c 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tournament.Components if (stream != null) { - InternalChild = video = new VideoSprite(stream) + InternalChild = video = new VideoSprite(stream, false) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 4e51ff939a..be5762e68d 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -259,11 +259,18 @@ namespace osu.Game.Screens.Menu private class LazerLogo : CompositeDrawable { + private readonly Stream videoStream; + public LazerLogo(Stream videoStream) { + this.videoStream = videoStream; Size = new Vector2(960); + } - InternalChild = new VideoSprite(videoStream) + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new VideoSprite(videoStream, false) { RelativeSizeAxes = Axes.Both, Clock = new FramedOffsetClock(Clock) { Offset = -logo_1 } From 758bb3711f6f7105296a1922b0108a4129743013 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:07:11 +0900 Subject: [PATCH 203/387] Add more sane limit for maximum slider length --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 1fc51d2ce8..8d3ad5984f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 7) { - length = Math.Max(0, Parsing.ParseDouble(split[7])); + length = Math.Max(0, Parsing.ParseDouble(split[7], Parsing.MAX_COORDINATE_VALUE)); if (length == 0) length = null; } From 9667934ed9d0a7a44f55af4c7515fdb0df3bded8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:17:32 +0900 Subject: [PATCH 204/387] Remove unlimited timing points in difficulty calculation --- .../LegacyDifficultyCalculatorBeatmapDecoder.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index 527f520172..bf52a87865 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -26,17 +26,5 @@ namespace osu.Game.Beatmaps.Formats AddDecoder(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last()))); SetFallbackDecoder(() => new LegacyDifficultyCalculatorBeatmapDecoder()); } - - protected override TimingControlPoint CreateTimingControlPoint() - => new LegacyDifficultyCalculatorTimingControlPoint(); - - private class LegacyDifficultyCalculatorTimingControlPoint : TimingControlPoint - { - public LegacyDifficultyCalculatorTimingControlPoint() - { - BeatLengthBindable.MinValue = double.MinValue; - BeatLengthBindable.MaxValue = double.MaxValue; - } - } } } From 40ab860ab56f7b89264c0c7c3475d680a09c0951 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:23:30 +0900 Subject: [PATCH 205/387] Remove unused using --- .../Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index bf52a87865..3420fcf260 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Beatmaps.Formats { From 5b03b3e36304b1546c671aa875ce3d97bec79ced Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:36:37 +0900 Subject: [PATCH 206/387] Fix hyperdashes not recalculated with HR application --- .../Beatmaps/CatchBeatmapProcessor.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 1a5d0f983b..e0c65a8317 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -28,8 +28,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps ApplyPositionOffsets(Beatmap); - initialiseHyperDash((List)Beatmap.HitObjects); - int index = 0; foreach (var obj in Beatmap.HitObjects.OfType()) @@ -90,6 +88,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps break; } } + + initialiseHyperDash(beatmap); } private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) @@ -191,14 +191,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } } - private void initialiseHyperDash(List objects) + private static void initialiseHyperDash(IBeatmap beatmap) { List objectWithDroplets = new List(); - foreach (var currentObject in objects) + foreach (var currentObject in beatmap.HitObjects) { - if (currentObject is Fruit) - objectWithDroplets.Add(currentObject); + if (currentObject is Fruit fruitObject) + objectWithDroplets.Add(fruitObject); if (currentObject is JuiceStream) { @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); - double halfCatcherWidth = CatcherArea.GetCatcherSize(Beatmap.BeatmapInfo.BaseDifficulty) / 2; + double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2; int lastDirection = 0; double lastExcess = halfCatcherWidth; @@ -221,6 +221,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps CatchHitObject currentObject = objectWithDroplets[i]; CatchHitObject nextObject = objectWithDroplets[i + 1]; + // Reset variables in-case values have changed (e.g. after applying HR) + currentObject.HyperDashTarget = null; + currentObject.DistanceToHyperDash = 0; + int thisDirection = nextObject.X > currentObject.X ? 1 : -1; double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); From f8e7579f4574aee46ae278fe853e43864d047710 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:37:58 +0900 Subject: [PATCH 207/387] Fix juice stream position reset not ever being applied --- .../Beatmaps/CatchBeatmapProcessor.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index e0c65a8317..e76e95e9aa 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -74,6 +74,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps break; case JuiceStream juiceStream: + // Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead. + lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X / CatchPlayfield.BASE_WIDTH; + + // Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead. + lastStartTime = juiceStream.StartTime; + foreach (var nested in juiceStream.NestedHitObjects) { var catchObject = (CatchHitObject)nested; @@ -94,13 +100,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) { - if (hitObject is JuiceStream stream) - { - lastPosition = stream.EndX; - lastStartTime = stream.EndTime; - return; - } - if (!(hitObject is Fruit)) return; From 919410c6277644329b96a6724a5045b6a94072ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:39:47 +0900 Subject: [PATCH 208/387] Remove always-false condition --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index e76e95e9aa..5f23bf1428 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -100,9 +100,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) { - if (!(hitObject is Fruit)) - return; - float offsetPosition = hitObject.X; double startTime = hitObject.StartTime; From 5c051027e707db7fea7bf6a11b121249d44afd70 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:43:08 +0900 Subject: [PATCH 209/387] Fix different offset being applied from stable --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 5f23bf1428..986dc9dbb9 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -112,7 +112,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } float positionDiff = offsetPosition - lastPosition.Value; - double timeDiff = startTime - lastStartTime; + + // Todo: BUG!! Stable calculated time deltas as ints, which affects randomisation. This should be changed to a double. + int timeDiff = (int)(startTime - lastStartTime); if (timeDiff > 1000) { @@ -128,7 +130,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps return; } - if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d) + // ReSharper disable once PossibleLossOfFraction + if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3) applyOffset(ref offsetPosition, positionDiff); hitObject.XOffset = offsetPosition - hitObject.X; From 2866d626531f678e07c81b44ab27d7fecae95d87 Mon Sep 17 00:00:00 2001 From: Olle Kelderman Date: Wed, 11 Mar 2020 16:17:28 +0100 Subject: [PATCH 210/387] Use environment variable for initializing osuInstallPath --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index b19f2bedf0..eefa9fcfe6 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -163,12 +163,7 @@ namespace osu.Game.Tournament.IPC { try { - stableInstallPath = "G:\\My Drive\\Main\\osu!tourney"; - - if (checkExists(stableInstallPath)) - return stableInstallPath; - - stableInstallPath = "G:\\My Drive\\Main\\osu!mappool"; + stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); if (checkExists(stableInstallPath)) return stableInstallPath; From efceeba0769999db723bb74d0f0a2b5c143270c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 02:22:02 +0900 Subject: [PATCH 211/387] Use fixed width for tournament score displays --- .../Screens/Gameplay/Components/MatchScoreDisplay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs index ed14956793..2e7484542a 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; +using osuTK; namespace osu.Game.Tournament.Screens.Gameplay.Components { @@ -131,13 +132,15 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; Winning = false; + + DisplayedCountSpriteText.Spacing = new Vector2(-6); } public bool Winning { set => DisplayedCountSpriteText.Font = value - ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50) - : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40); + ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true) + : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true); } } } From 09b9983286d5a1a7f960ed968f33a2b3bc139c67 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 11 Mar 2020 21:14:07 +0300 Subject: [PATCH 212/387] Fix CatcherAnimationState is Fail if missing banana shower --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 9ee94636f1..d18f5e165f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -345,7 +345,10 @@ namespace osu.Game.Rulesets.Catch.UI if (validCatch) updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); else - updateState(CatcherAnimationState.Fail); + { + if (!(fruit is Banana)) + updateState(CatcherAnimationState.Fail); + } return validCatch; } From e46c070d951341370f16733da1d63c33c0f116c6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 11 Mar 2020 23:09:29 +0300 Subject: [PATCH 213/387] Add test scene --- .../TestSceneDrawableHitObjects.cs | 53 +++++++++++++++---- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 10 ++-- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 070847c0c1..304c7e3854 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Catch.Tests private DrawableCatchRuleset drawableRuleset; private double playfieldTime => drawableRuleset.Playfield.Time.Current; - [BackgroundDependencyLoader] - private void load() + [SetUp] + public void Setup() => Schedule(() => { var controlPointInfo = new ControlPointInfo(); controlPointInfo.Add(0, new TimingControlPoint()); @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Catch.Tests ControlPointInfo = controlPointInfo }); - Add(new Container + Child = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -66,16 +66,49 @@ namespace osu.Game.Rulesets.Catch.Tests { drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), beatmap.GetPlayableBeatmap(new CatchRuleset().RulesetInfo)) } - }); + }; + }); + + [Test] + public void TestFruits() + { + AddStep("hit fruits", () => spawnFruits(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); AddStep("miss fruits", () => spawnFruits()); - AddStep("hit fruits", () => spawnFruits(true)); - AddStep("miss juicestream", () => spawnJuiceStream()); - AddStep("hit juicestream", () => spawnJuiceStream(true)); - AddStep("miss bananas", () => spawnBananas()); - AddStep("hit bananas", () => spawnBananas(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail); } + [Test] + public void TestJuicestream() + { + AddStep("hit juicestream", () => spawnJuiceStream(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + + AddStep("miss juicestream", () => spawnJuiceStream()); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail); + } + + [Test] + public void TestBananas() + { + AddStep("hit bananas", () => spawnBananas(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + + AddStep("miss bananas", () => spawnBananas()); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + } + + private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive); + + private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).CatcherArea.MovableCatcher.CurrentState; + private void spawnFruits(bool hit = false) { for (int i = 1; i <= 4; i++) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index d18f5e165f..441f9126f6 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Catch.UI CatcherSprite current; - switch (currentState) + switch (CurrentState) { default: current = catcherIdle; @@ -274,7 +274,7 @@ namespace osu.Game.Rulesets.Catch.UI return additive; } - private Drawable createCatcherSprite() => new CatcherSprite(currentState); + private Drawable createCatcherSprite() => new CatcherSprite(CurrentState); /// /// Add a caught fruit to the catcher's stack. @@ -355,14 +355,14 @@ namespace osu.Game.Rulesets.Catch.UI private void updateState(CatcherAnimationState state) { - if (currentState == state) + if (CurrentState == state) return; - currentState = state; + CurrentState = state; updateCatcher(); } - private CatcherAnimationState currentState; + public CatcherAnimationState CurrentState; private double hyperDashModifier = 1; private int hyperDashDirection; From fd21e87670ebde3018e43a550aff6ef9b30383f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 11:28:45 +0900 Subject: [PATCH 214/387] Disable adjusting volume via "select next" and "select previous" as fallbacks --- .../Overlays/Volume/VolumeControlReceptor.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 4bff8146b4..3478f18a40 100644 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -14,22 +14,8 @@ namespace osu.Game.Overlays.Volume public Func ActionRequested; public Func ScrollActionRequested; - public bool OnPressed(GlobalAction action) - { - // if nothing else handles selection actions in the game, it's safe to let volume be adjusted. - switch (action) - { - case GlobalAction.SelectPrevious: - action = GlobalAction.IncreaseVolume; - break; - - case GlobalAction.SelectNext: - action = GlobalAction.DecreaseVolume; - break; - } - - return ActionRequested?.Invoke(action) ?? false; - } + public bool OnPressed(GlobalAction action) => + ActionRequested?.Invoke(action) ?? false; public bool OnScroll(GlobalAction action, float amount, bool isPrecise) => ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false; From 39bb98bfb219a2c174c1193850ce500eb0b5aeba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 13:26:58 +0900 Subject: [PATCH 215/387] Allow videos to be loaded with any extension Also moves all tournament user resources to a "tournament" subfolder. --- .../Components/TourneyVideo.cs | 5 ++--- osu.Game.Tournament/TournamentGameBase.cs | 6 +++++- osu.Game.Tournament/TournamentStorage.cs | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tournament/TournamentStorage.cs diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 43088d6b92..d8488ce4f6 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Video; -using osu.Framework.Platform; using osu.Framework.Timing; using osu.Game.Graphics; @@ -28,9 +27,9 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(Storage storage) + private void load(TournamentStorage storage) { - var stream = storage.GetStream($@"videos/{filename}.m4v"); + var stream = storage.GetStream($@"videos/{filename}"); if (stream != null) { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 41165ca141..41822ae2c3 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -37,6 +37,8 @@ namespace osu.Game.Tournament private Storage storage; + private TournamentStorage tournamentStorage; + private DependencyContainer dependencies; private Bindable windowSize; @@ -54,7 +56,9 @@ namespace osu.Game.Tournament { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - Textures.AddStore(new TextureLoaderStore(new ResourceStore(new StorageBackedResourceStore(storage)))); + dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage)); + + Textures.AddStore(new TextureLoaderStore(tournamentStorage)); this.storage = storage; diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs new file mode 100644 index 0000000000..139ad3857b --- /dev/null +++ b/osu.Game.Tournament/TournamentStorage.cs @@ -0,0 +1,19 @@ +// 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.IO.Stores; +using osu.Framework.Platform; + +namespace osu.Game.Tournament +{ + internal class TournamentStorage : NamespacedResourceStore + { + public TournamentStorage(Storage storage) + : base(new StorageBackedResourceStore(storage), "tournament") + { + AddExtension("m4v"); + AddExtension("avi"); + AddExtension("mp4"); + } + } +} From 190ff974862e9e379917eeccd40034c546448980 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 13:29:09 +0900 Subject: [PATCH 216/387] Rename classes to better suit purpose --- .../Components/DrawableTournamentHeaderLogo.cs | 18 ++++++++++++++++++ .../Components/DrawableTournamentHeaderText.cs | 18 ++++++++++++++++++ .../Components/DrawableTournamentTitleText.cs | 16 ---------------- osu.Game.Tournament/Components/RoundDisplay.cs | 2 +- .../Screens/Gameplay/Components/MatchHeader.cs | 4 ++-- .../{RoundDisplay.cs => MatchRoundDisplay.cs} | 2 +- .../Screens/Ladder/LadderScreen.cs | 2 +- .../Screens/Schedule/ScheduleScreen.cs | 2 +- 8 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs create mode 100644 osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs delete mode 100644 osu.Game.Tournament/Components/DrawableTournamentTitleText.cs rename osu.Game.Tournament/Screens/Gameplay/Components/{RoundDisplay.cs => MatchRoundDisplay.cs} (92%) diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs new file mode 100644 index 0000000000..b6f74a75e7 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -0,0 +1,18 @@ +// 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.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTournamentHeaderLogo : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get("header-text"); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs new file mode 100644 index 0000000000..38500fa857 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -0,0 +1,18 @@ +// 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.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTournamentHeaderText : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get("header-text"); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs b/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs deleted file mode 100644 index 4fbc6cd060..0000000000 --- a/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; - -namespace osu.Game.Tournament.Components -{ - public class DrawableTournamentTitleText : TournamentSpriteText - { - public DrawableTournamentTitleText() - { - Text = "osu!taiko world cup 2020"; - Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold); - } - } -} diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs index dd56c83c57..bebede6782 100644 --- a/osu.Game.Tournament/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Components/RoundDisplay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Components Direction = FillDirection.Vertical, Children = new Drawable[] { - new DrawableTournamentTitleText(), + new DrawableTournamentHeaderText(), new TournamentSpriteText { Text = match.Round.Value?.Name.Value ?? "Unknown Round", diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 69a68c946b..aa4bd4a701 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -41,13 +41,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Spacing = new Vector2(5), Children = new Drawable[] { - new DrawableTournamentTitleText + new DrawableTournamentHeaderText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.2f) }, - new RoundDisplay + new MatchRoundDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs similarity index 92% rename from osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs rename to osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs index c8b0d3bdda..87793f7e1b 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs @@ -8,7 +8,7 @@ using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Gameplay.Components { - public class RoundDisplay : TournamentSpriteTextWithBackground + public class MatchRoundDisplay : TournamentSpriteTextWithBackground { private readonly Bindable currentMatch = new Bindable(); diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 6f62b3ddba..534c402f6c 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tournament.Screens.Ladder RelativeSizeAxes = Axes.Both, Loop = true, }, - new DrawableTournamentTitleText + new DrawableTournamentHeaderText { Y = 100, Anchor = Anchor.TopCentre, diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 0fcec645e3..88289ad6bd 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tournament.Screens.Schedule Direction = FillDirection.Vertical, Children = new Drawable[] { - new DrawableTournamentTitleText(), + new DrawableTournamentHeaderText(), new Container { Margin = new MarginPadding { Top = 40 }, From b6b802e8212f44a669675f6705fd16db667fd758 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 14:06:40 +0900 Subject: [PATCH 217/387] Add back customisable header logo/text Also adds test scene for MatchHeader component. --- .../Components/TestSceneMatchHeader.cs | 33 +++++ .../DrawableTournamentHeaderLogo.cs | 27 +++- .../DrawableTournamentHeaderText.cs | 27 +++- .../Gameplay/Components/MatchHeader.cs | 119 +++++++----------- .../Gameplay/Components/TeamScoreDisplay.cs | 83 ++++++++++++ 5 files changed, 206 insertions(+), 83 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs create mode 100644 osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs new file mode 100644 index 0000000000..b29e4964b6 --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs @@ -0,0 +1,33 @@ +// 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.Game.Graphics; +using osu.Game.Tournament.Screens.Gameplay.Components; +using osuTK; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneMatchHeader : TournamentTestScene + { + public TestSceneMatchHeader() + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(50), + Children = new Drawable[] + { + new TournamentSpriteText { Text = "with logo", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader(), + new TournamentSpriteText { Text = "without logo", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader { ShowLogo = false }, + new TournamentSpriteText { Text = "without scores", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader { ShowScores = false }, + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs index b6f74a75e7..a61cb59fed 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -2,17 +2,36 @@ // 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.Sprites; using osu.Framework.Graphics.Textures; namespace osu.Game.Tournament.Components { - public class DrawableTournamentHeaderLogo : Sprite + public class DrawableTournamentHeaderLogo : CompositeDrawable { - [BackgroundDependencyLoader] - private void load(TextureStore textures) + public DrawableTournamentHeaderLogo() { - Texture = textures.Get("header-text"); + InternalChild = new LogoSprite(); + + Height = 50; + RelativeSizeAxes = Axes.X; + } + + private class LogoSprite : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Texture = textures.Get("header-logo"); + } } } } diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs index 38500fa857..2539075c0f 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -2,17 +2,36 @@ // 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.Sprites; using osu.Framework.Graphics.Textures; namespace osu.Game.Tournament.Components { - public class DrawableTournamentHeaderText : Sprite + public class DrawableTournamentHeaderText : CompositeDrawable { - [BackgroundDependencyLoader] - private void load(TextureStore textures) + public DrawableTournamentHeaderText() { - Texture = textures.Get("header-text"); + InternalChild = new TextSprite(); + + Height = 25; + RelativeSizeAxes = Axes.X; + } + + private class TextSprite : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Texture = textures.Get("header-text"); + } } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index aa4bd4a701..751a763333 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -2,14 +2,11 @@ // 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.Events; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Input; namespace osu.Game.Tournament.Screens.Gameplay.Components { @@ -17,13 +14,39 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private TeamScoreDisplay teamDisplay1; private TeamScoreDisplay teamDisplay2; + private DrawableTournamentHeaderLogo logo; + + private bool showScores = true; public bool ShowScores { + get => showScores; set { - teamDisplay1.ShowScore = value; - teamDisplay2.ShowScore = value; + if (value == showScores) + return; + + showScores = value; + + if (IsLoaded) + updateDisplay(); + } + } + + private bool showLogo = true; + + public bool ShowLogo + { + get => showLogo; + set + { + if (value == showLogo) + return; + + showLogo = value; + + if (IsLoaded) + updateDisplay(); } } @@ -38,19 +61,25 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, + Padding = new MarginPadding(5), Spacing = new Vector2(5), Children = new Drawable[] { + logo = new DrawableTournamentHeaderLogo + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Alpha = showLogo ? 1 : 0 + }, new DrawableTournamentHeaderText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.2f) + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, }, new MatchRoundDisplay { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Scale = new Vector2(0.4f) }, } @@ -66,76 +95,16 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Origin = Anchor.TopRight, }, }; - } - } - public class TeamScoreDisplay : CompositeDrawable - { - private readonly TeamColour teamColour; - - private readonly Bindable currentMatch = new Bindable(); - private readonly Bindable currentTeam = new Bindable(); - private readonly Bindable currentTeamScore = new Bindable(); - - private TeamDisplay teamDisplay; - - public bool ShowScore { set => teamDisplay.ShowScore = value; } - - public TeamScoreDisplay(TeamColour teamColour) - { - this.teamColour = teamColour; - - RelativeSizeAxes = Axes.Y; - AutoSizeAxes = Axes.X; + updateDisplay(); } - [BackgroundDependencyLoader] - private void load(LadderInfo ladder) + private void updateDisplay() { - currentMatch.BindTo(ladder.CurrentMatch); - currentMatch.BindValueChanged(matchChanged, true); - } + teamDisplay1.ShowScore = showScores; + teamDisplay2.ShowScore = showScores; - private void matchChanged(ValueChangedEvent match) - { - currentTeamScore.UnbindBindings(); - currentTeam.UnbindBindings(); - - if (match.NewValue != null) - { - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); - } - - // team may change to same team, which means score is not in a good state. - // thus we handle this manually. - teamChanged(currentTeam.Value); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - switch (e.Button) - { - case MouseButton.Left: - if (currentTeamScore.Value < currentMatch.Value.PointsToWin) - currentTeamScore.Value++; - return true; - - case MouseButton.Right: - if (currentTeamScore.Value > 0) - currentTeamScore.Value--; - return true; - } - - return base.OnMouseDown(e); - } - - private void teamChanged(TournamentTeam team) - { - InternalChildren = new Drawable[] - { - teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), - }; + logo.Alpha = showLogo ? 1 : 0; } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs new file mode 100644 index 0000000000..462015f004 --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -0,0 +1,83 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Tournament.Models; +using osuTK.Input; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class TeamScoreDisplay : CompositeDrawable + { + private readonly TeamColour teamColour; + + private readonly Bindable currentMatch = new Bindable(); + private readonly Bindable currentTeam = new Bindable(); + private readonly Bindable currentTeamScore = new Bindable(); + + private TeamDisplay teamDisplay; + + public bool ShowScore { set => teamDisplay.ShowScore = value; } + + public TeamScoreDisplay(TeamColour teamColour) + { + this.teamColour = teamColour; + + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(LadderInfo ladder) + { + currentMatch.BindTo(ladder.CurrentMatch); + currentMatch.BindValueChanged(matchChanged, true); + } + + private void matchChanged(ValueChangedEvent match) + { + currentTeamScore.UnbindBindings(); + currentTeam.UnbindBindings(); + + if (match.NewValue != null) + { + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + } + + // team may change to same team, which means score is not in a good state. + // thus we handle this manually. + teamChanged(currentTeam.Value); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + switch (e.Button) + { + case MouseButton.Left: + if (currentTeamScore.Value < currentMatch.Value.PointsToWin) + currentTeamScore.Value++; + return true; + + case MouseButton.Right: + if (currentTeamScore.Value > 0) + currentTeamScore.Value--; + return true; + } + + return base.OnMouseDown(e); + } + + private void teamChanged(TournamentTeam team) + { + InternalChildren = new Drawable[] + { + teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), + }; + } + } +} From 7b1ac03b18dae6767ee4c13be887b33913570f31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 14:26:22 +0900 Subject: [PATCH 218/387] Hide logo on gameplay screen --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 4d770855cd..8920990d1b 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -47,7 +47,10 @@ namespace osu.Game.Tournament.Screens.Gameplay Loop = true, RelativeSizeAxes = Axes.Both, }, - header = new MatchHeader(), + header = new MatchHeader + { + ShowLogo = false + }, new Container { RelativeSizeAxes = Axes.X, From ec1c6f88ee3889cbe9657e5745a4753255b7cbf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 14:26:39 +0900 Subject: [PATCH 219/387] Adjust metrics to align logo pieces correctly on gameplay / map pool --- .../Components/TestSceneMatchHeader.cs | 9 +++++++++ .../Components/DrawableTournamentHeaderLogo.cs | 2 +- .../Components/DrawableTournamentHeaderText.cs | 2 +- .../Screens/Gameplay/Components/MatchHeader.cs | 2 +- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 3 ++- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs index b29e4964b6..9f885ed827 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Gameplay.Components; using osuTK; @@ -11,6 +14,12 @@ namespace osu.Game.Tournament.Tests.Components { public class TestSceneMatchHeader : TournamentTestScene { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableTournamentHeaderText), + typeof(DrawableTournamentHeaderLogo), + }; + public TestSceneMatchHeader() { Child = new FillFlowContainer diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs index a61cb59fed..3f5ab42fd7 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Components { InternalChild = new LogoSprite(); - Height = 50; + Height = 82; RelativeSizeAxes = Axes.X; } diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs index 2539075c0f..bda696ba00 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Components { InternalChild = new TextSprite(); - Height = 25; + Height = 22; RelativeSizeAxes = Axes.X; } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 751a763333..d790f4b754 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Padding = new MarginPadding(5), + Padding = new MarginPadding(20), Spacing = new Vector2(5), Children = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 4f3f7cfdbf..2b0bfe0b74 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader(), mapFlows = new FillFlowContainer> { - Y = 100, + Y = 140, Spacing = new Vector2(10, 10), Padding = new MarginPadding(25), Direction = FillDirection.Vertical, @@ -235,6 +235,7 @@ namespace osu.Game.Tournament.Screens.MapPool { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + Height = 42, }); } } From 63edcddaf13453d66e86d4368e63ed90d2636962 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:01:43 +0900 Subject: [PATCH 220/387] Apply ruleset filter in all cases (even when bypassing filter for selection purposes) --- .../Screens/Select/Carousel/CarouselBeatmap.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 8c264ce974..e0d59e3b18 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -25,18 +25,18 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) - { - // bypass filtering for selected beatmap - Filtered.Value = false; - return; - } - bool match = criteria.Ruleset == null || Beatmap.RulesetID == criteria.Ruleset.ID || (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); + if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) + { + // bypass filtering for selected beatmap + Filtered.Value = !match; + return; + } + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty); match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate); From 28ac5af91c9bc751f66eb634c0fb3b98e76ba5ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:26:22 +0900 Subject: [PATCH 221/387] Fix beatmap carousel tests loading beatmap manager beatmaps in test browser --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 4 +++- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 71ae47dc66..80e03d82e2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -497,7 +497,7 @@ namespace osu.Game.Tests.Visual.SongSelect } bool changed = false; - AddStep($"Load {beatmapSets.Count} Beatmaps", () => + AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () => { carousel.Filter(new FilterCriteria()); carousel.BeatmapSetsChanged = () => changed = true; @@ -697,6 +697,8 @@ namespace osu.Game.Tests.Visual.SongSelect public new List Items => base.Items; public bool PendingFilterTask => PendingFilter != null; + + protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty(); } } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 71744d8b80..04c08cdbd2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -153,9 +153,11 @@ namespace osu.Game.Screens.Select beatmaps.BeatmapHidden += beatmapHidden; beatmaps.BeatmapRestored += beatmapRestored; - loadBeatmapSets(beatmaps.GetAllUsableBeatmapSetsEnumerable()); + loadBeatmapSets(GetLoadableBeatmaps()); } + protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); From 933a8ffc8a8152e6e5680d39557bd3e2958f407b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 17:10:51 +0900 Subject: [PATCH 222/387] Add test coverage --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 16 +++++++++++++++- .../Carousel/DrawableCarouselBeatmapSet.cs | 8 +++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 105d96cdfe..fb287a4f70 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -436,6 +436,9 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(0); + // used for filter check below + AddStep("allow convert display", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true)); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); @@ -446,9 +449,11 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapInfo target = null; + int targetRuleset = differentRuleset ? 1 : 0; + AddStep("select beatmap externally", () => { - target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == (differentRuleset ? 1 : 0))) + target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) .ElementAt(5).Beatmaps.First(); Beatmap.Value = manager.GetWorkingBeatmap(target); @@ -456,6 +461,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddAssert("selected only shows expected ruleset (plus converts)", () => + { + var selectedPanel = songSelect.Carousel.ChildrenOfType().First(s => s.Item.State.Value == CarouselItemState.Selected); + + // special case for converts checked here. + return selectedPanel.ChildrenOfType().All(i => + i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0); + }); + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 6cd145cfef..1454784f03 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -205,7 +205,9 @@ namespace osu.Game.Screens.Select.Carousel { private readonly BindableBool filtered = new BindableBool(); - private readonly CarouselBeatmap item; + public bool IsFiltered => filtered.Value; + + public readonly CarouselBeatmap Item; public FilterableDifficultyIcon(CarouselBeatmap item) : base(item.Beatmap) @@ -214,13 +216,13 @@ namespace osu.Game.Screens.Select.Carousel filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); filtered.TriggerChange(); - this.item = item; + this.Item = item; } protected override bool OnClick(ClickEvent e) { if (!filtered.Value) - item.State.Value = CarouselItemState.Selected; + Item.State.Value = CarouselItemState.Selected; return true; } From fc058f8896eb8f023b6e7702d21355167f1a78b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 18:03:18 +0900 Subject: [PATCH 223/387] Remove unnecessary this. prefix --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 1454784f03..547aeaddc6 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Select.Carousel filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); filtered.TriggerChange(); - this.Item = item; + Item = item; } protected override bool OnClick(ClickEvent e) From bc2a1cdb623547e9a05740685eba26f75bd00c76 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 12 Mar 2020 12:04:36 +0300 Subject: [PATCH 224/387] Apply suggestions --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 441f9126f6..2394110165 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -344,11 +344,8 @@ namespace osu.Game.Rulesets.Catch.UI if (validCatch) updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); - else - { - if (!(fruit is Banana)) - updateState(CatcherAnimationState.Fail); - } + else if (!(fruit is Banana)) + updateState(CatcherAnimationState.Fail); return validCatch; } @@ -362,7 +359,7 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - public CatcherAnimationState CurrentState; + public CatcherAnimationState CurrentState { get; private set; } private double hyperDashModifier = 1; private int hyperDashDirection; From 5537b279de1b3707496f4bf8aac49aa359f13cbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 18:39:43 +0900 Subject: [PATCH 225/387] Fix failing test occasionally getting wrong ruleset beatmap --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index fb287a4f70..55c1d8451f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -453,8 +453,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select beatmap externally", () => { - target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) - .ElementAt(5).Beatmaps.First(); + target = manager.GetAllUsableBeatmapSets() + .Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) + .ElementAt(5).Beatmaps.First(bi => bi.RulesetID == targetRuleset); Beatmap.Value = manager.GetWorkingBeatmap(target); }); From ce5d01ed191ea896b5b643352b791d27615045b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 17:55:31 +0900 Subject: [PATCH 226/387] Allow filtered difficulty icons to be clicked --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 547aeaddc6..d3a7b4d3d9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -221,9 +221,7 @@ namespace osu.Game.Screens.Select.Carousel protected override bool OnClick(ClickEvent e) { - if (!filtered.Value) - Item.State.Value = CarouselItemState.Selected; - + Item.State.Value = CarouselItemState.Selected; return true; } } From 2bcf07938676b39a0db435939547500c9a562aca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:34:58 +0900 Subject: [PATCH 227/387] Update carousel test logic to match new carousel selection behaviour --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 71ae47dc66..d80add3015 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -399,7 +399,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); - AddAssert("unfiltered beatmap selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0])); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap == null); AddStep("remove mixed set", () => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 55c1d8451f..d16fd0bceb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -591,16 +591,16 @@ namespace osu.Game.Tests.Visual.SongSelect } })); + BeatmapInfo filteredBeatmap = null; DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null; + AddStep("Get filtered icon", () => { - var filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); + filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); }); - int? previousID = null; - AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID); AddStep("Click on a filtered difficulty", () => { InputManager.MoveMouseTo(filteredIcon); @@ -608,7 +608,8 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.PressButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID); + + AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); } private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); From 6e11c3014ce0830390957797a024d1978d45122e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 16:30:21 +0900 Subject: [PATCH 228/387] Allow grouped difficulty icons to be clicked --- .../SongSelect/TestScenePlaySongSelect.cs | 48 +++++++++++++++++-- .../Carousel/DrawableCarouselBeatmapSet.cs | 12 +++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 8295f0aa66..34f442e6ee 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -654,6 +654,48 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); } + [Test] + public void TestGroupedDifficultyIconSelecting() + { + changeRuleset(0); + + createSongSelect(); + + AddStep("import huge difficulty count map", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); + manager.Import(createTestBeatmapSet(0, usableRulesets, 50)).Wait(); + }); + + DrawableCarouselBeatmapSet set = null; + AddUntilStep("Find the DrawableCarouselBeatmapSet", () => + { + set = songSelect.Carousel.ChildrenOfType().FirstOrDefault(); + return set != null; + }); + + DrawableCarouselBeatmapSet.FilterableGroupedDifficultyIcon groupIcon = null; + AddStep("Find group icon for different ruleset", () => + { + groupIcon = set.ChildrenOfType() + .First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3); + }); + + AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); + + AddStep("Click on group", () => + { + InputManager.MoveMouseTo(groupIcon); + + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); + + AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo == groupIcon.Items.First().Beatmap); + } + private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); @@ -695,16 +737,16 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - private BeatmapSetInfo createTestBeatmapSet(int setId, RulesetInfo[] rulesets) + private BeatmapSetInfo createTestBeatmapSet(int setId, RulesetInfo[] rulesets, int countPerRuleset = 6) { int j = 0; RulesetInfo getRuleset() => rulesets[j++ % rulesets.Length]; var beatmaps = new List(); - for (int i = 0; i < 6; i++) + for (int i = 0; i < countPerRuleset; i++) { - int beatmapId = setId * 10 + i; + int beatmapId = setId * 100 + i; int length = RNG.Next(30000, 200000); double bpm = RNG.NextSingle(80, 200); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index d3a7b4d3d9..a53b74c1b8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -228,12 +228,12 @@ namespace osu.Game.Screens.Select.Carousel public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon { - private readonly List items; + public readonly List Items; public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) : base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White) { - this.items = items; + Items = items; foreach (var item in items) item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay)); @@ -241,10 +241,16 @@ namespace osu.Game.Screens.Select.Carousel updateFilteredDisplay(); } + protected override bool OnClick(ClickEvent e) + { + Items.First().State.Value = CarouselItemState.Selected; + return true; + } + private void updateFilteredDisplay() { // for now, fade the whole group based on the ratio of hidden items. - this.FadeTo(1 - 0.9f * ((float)items.Count(i => i.Filtered.Value) / items.Count), 100); + this.FadeTo(1 - 0.9f * ((float)Items.Count(i => i.Filtered.Value) / Items.Count), 100); } } } From ca9cfbe51d50a7d9b95096efee9b519d8a607ae3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:52:03 +0900 Subject: [PATCH 229/387] Move selection fallback logic out of BeatmapCarousel to SongSelect --- osu.Game/Screens/Select/BeatmapCarousel.cs | 26 +++++++++----------- osu.Game/Screens/Select/SongSelect.cs | 28 +++++++++++++++------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 71744d8b80..ca20b02bce 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -225,25 +225,21 @@ namespace osu.Game.Screens.Select continue; if (!bypassFilters && item.Filtered.Value) - // The beatmap exists in this set but is filtered, so look for the first unfiltered map in the set - item = set.Beatmaps.FirstOrDefault(b => !b.Filtered.Value); + return false; - if (item != null) + select(item); + + // if we got here and the set is filtered, it means we were bypassing filters. + // in this case, reapplying the filter is necessary to ensure the panel is in the correct place + // (since it is forcefully being included in the carousel). + if (set.Filtered.Value) { - select(item); + Debug.Assert(bypassFilters); - // if we got here and the set is filtered, it means we were bypassing filters. - // in this case, reapplying the filter is necessary to ensure the panel is in the correct place - // (since it is forcefully being included in the carousel). - if (set.Filtered.Value) - { - Debug.Assert(bypassFilters); - - applyActiveCriteria(false); - } - - return true; + applyActiveCriteria(false); } + + return true; } return false; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 528222a89c..11c680bdb0 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -380,6 +380,8 @@ namespace osu.Game.Screens.Select { if (e.NewValue is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; + Logger.Log($"working beatmap updated to {e.NewValue}"); + if (!Carousel.SelectBeatmap(e.NewValue.BeatmapInfo, false)) { // A selection may not have been possible with filters applied. @@ -446,8 +448,10 @@ namespace osu.Game.Screens.Select if (transferRulesetValue()) { - // if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset. Mods.Value = Array.Empty(); + + // required to return once in order to have the carousel in a good state. + // if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset. return; } @@ -472,7 +476,7 @@ namespace osu.Game.Screens.Select if (this.IsCurrentScreen()) ensurePlayingSelected(); - UpdateBeatmap(Beatmap.Value); + updateComponentFromBeatmap(Beatmap.Value); } } @@ -547,7 +551,7 @@ namespace osu.Game.Screens.Select if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) { - UpdateBeatmap(Beatmap.Value); + updateComponentFromBeatmap(Beatmap.Value); // restart playback on returning to song select, regardless. music?.Play(); @@ -610,10 +614,8 @@ namespace osu.Game.Screens.Select /// This is a debounced call (unlike directly binding to WorkingBeatmap.ValueChanged). /// /// The working beatmap. - protected virtual void UpdateBeatmap(WorkingBeatmap beatmap) + private void updateComponentFromBeatmap(WorkingBeatmap beatmap) { - Logger.Log($"working beatmap updated to {beatmap}"); - if (Background is BackgroundScreenBeatmap backgroundModeBeatmap) { backgroundModeBeatmap.Beatmap = beatmap; @@ -658,9 +660,17 @@ namespace osu.Game.Screens.Select return; // Attempt to select the current beatmap on the carousel, if it is valid to be selected. - if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false - && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) - return; + if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false) + { + if (Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) + return; + + // prefer not changing ruleset at this point, so look for another difficulty in the currently playing beatmap + var found = Beatmap.Value.BeatmapSetInfo.Beatmaps.FirstOrDefault(b => b.Ruleset.Equals(decoupledRuleset.Value)); + + if (found != null && Carousel.SelectBeatmap(found, false)) + return; + } // If the current active beatmap could not be selected, select a new random beatmap. if (!Carousel.SelectNextRandom()) From db5c8043db509c2b94d46160a14616c359f52f35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 16:42:26 +0900 Subject: [PATCH 230/387] Add test covering ruleset change on difficulty icon selection --- .../SongSelect/TestScenePlaySongSelect.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d16fd0bceb..8295f0aa66 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -572,6 +572,7 @@ namespace osu.Game.Tests.Visual.SongSelect difficultyIcon = set.ChildrenOfType() .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); }); + AddStep("Click on a difficulty", () => { InputManager.MoveMouseTo(difficultyIcon); @@ -579,6 +580,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.PressButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left); }); + AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon)); double? maxBPM = null; @@ -596,7 +598,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Get filtered icon", () => { - filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); + filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); }); @@ -612,6 +614,46 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); } + [Test] + public void TestDifficultyIconSelectingForDifferentRuleset() + { + changeRuleset(0); + + createSongSelect(); + + AddStep("import multi-ruleset map", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); + manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait(); + }); + + DrawableCarouselBeatmapSet set = null; + AddUntilStep("Find the DrawableCarouselBeatmapSet", () => + { + set = songSelect.Carousel.ChildrenOfType().FirstOrDefault(); + return set != null; + }); + + DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null; + AddStep("Find an icon for different ruleset", () => + { + difficultyIcon = set.ChildrenOfType() + .First(icon => icon.Item.Beatmap.ID == 3); + }); + + AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); + + AddStep("Click on a difficulty", () => + { + InputManager.MoveMouseTo(difficultyIcon); + + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); + } + private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); From a69fabbd1ff4255c7cad25881b31daa5a48f365e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 18:56:48 +0900 Subject: [PATCH 231/387] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 6a8e66ee6a..f623a92ade 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@
- + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index cc1ab654ab..ba6f0e2251 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 04b688cfa3..54cd400d51 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 1819a15509c8854a1307e9d86a8beb79fdefe59d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 18:56:31 +0900 Subject: [PATCH 232/387] Make test ID assigning simpler --- .../SongSelect/TestScenePlaySongSelect.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 34f442e6ee..62eb1340fc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -283,7 +283,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait(); + manager.Import(createTestBeatmapSet(usableRulesets)).Wait(); }); } else @@ -624,7 +624,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait(); + manager.Import(createTestBeatmapSet(usableRulesets)).Wait(); }); DrawableCarouselBeatmapSet set = null; @@ -638,7 +638,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.Beatmap.ID == 3); + .First(icon => icon.Item.Beatmap.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); @@ -664,7 +664,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - manager.Import(createTestBeatmapSet(0, usableRulesets, 50)).Wait(); + manager.Import(createTestBeatmapSet(usableRulesets, 50)).Wait(); }); DrawableCarouselBeatmapSet set = null; @@ -707,7 +707,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id)); - private void importForRuleset(int id) => manager.Import(createTestBeatmapSet(getImportId(), rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray())).Wait(); + private void importForRuleset(int id) => manager.Import(createTestBeatmapSet(rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray())).Wait(); private static int importId; @@ -733,20 +733,22 @@ namespace osu.Game.Tests.Visual.SongSelect var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); for (int i = 0; i < 100; i += 10) - manager.Import(createTestBeatmapSet(i, usableRulesets)).Wait(); + manager.Import(createTestBeatmapSet(usableRulesets)).Wait(); }); } - private BeatmapSetInfo createTestBeatmapSet(int setId, RulesetInfo[] rulesets, int countPerRuleset = 6) + private BeatmapSetInfo createTestBeatmapSet(RulesetInfo[] rulesets, int countPerRuleset = 6) { int j = 0; RulesetInfo getRuleset() => rulesets[j++ % rulesets.Length]; + int setId = getImportId(); + var beatmaps = new List(); for (int i = 0; i < countPerRuleset; i++) { - int beatmapId = setId * 100 + i; + int beatmapId = setId * 1000 + i; int length = RNG.Next(30000, 200000); double bpm = RNG.NextSingle(80, 200); From 250061ddf50d1a06541bbcaaf78fc6989b70ebec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 19:46:21 +0900 Subject: [PATCH 233/387] Fix test failure due to off-screen panel --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 62eb1340fc..9f33d03ac4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -661,12 +661,16 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); + BeatmapSetInfo imported = null; + AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - manager.Import(createTestBeatmapSet(usableRulesets, 50)).Wait(); + imported = manager.Import(createTestBeatmapSet(usableRulesets, 50)).Result; }); + AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); + DrawableCarouselBeatmapSet set = null; AddUntilStep("Find the DrawableCarouselBeatmapSet", () => { From bab197553e3968263cf0ee93776707d8da7c53c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:34:58 +0900 Subject: [PATCH 234/387] Update carousel test logic to match new carousel selection behaviour --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 71ae47dc66..d80add3015 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -399,7 +399,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); - AddAssert("unfiltered beatmap selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0])); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap == null); AddStep("remove mixed set", () => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 55c1d8451f..d16fd0bceb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -591,16 +591,16 @@ namespace osu.Game.Tests.Visual.SongSelect } })); + BeatmapInfo filteredBeatmap = null; DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null; + AddStep("Get filtered icon", () => { - var filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); + filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); }); - int? previousID = null; - AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID); AddStep("Click on a filtered difficulty", () => { InputManager.MoveMouseTo(filteredIcon); @@ -608,7 +608,8 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.PressButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID); + + AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); } private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); From 317bb5d0a436e3f6af94c141beeeec314b8f10cc Mon Sep 17 00:00:00 2001 From: Kelvin <2yangk23@gmail.com> Date: Thu, 12 Mar 2020 03:55:45 -0700 Subject: [PATCH 235/387] Fallback on invalid AnimationFramerate for legacy skins --- osu.Game/Skinning/LegacySkinExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index fa4de21eec..9cc58f4490 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -61,7 +61,7 @@ namespace osu.Game.Skinning { var iniRate = source.GetConfig(GlobalSkinConfiguration.AnimationFramerate); - if (iniRate != null) + if (iniRate != null && iniRate.Value > 0) return 1000f / iniRate.Value; return 1000f / textures.Length; From c8ea92257765d05bba2ae3d7a0f0966625d2aacf Mon Sep 17 00:00:00 2001 From: Kelvin <2yangk23@users.noreply.github.com> Date: Thu, 12 Mar 2020 04:18:57 -0700 Subject: [PATCH 236/387] Update osu.Game/Skinning/LegacySkinExtensions.cs Co-Authored-By: Dean Herbert --- osu.Game/Skinning/LegacySkinExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 9cc58f4490..52328d43b2 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -61,7 +61,7 @@ namespace osu.Game.Skinning { var iniRate = source.GetConfig(GlobalSkinConfiguration.AnimationFramerate); - if (iniRate != null && iniRate.Value > 0) + if (iniRate?.Value > 0) return 1000f / iniRate.Value; return 1000f / textures.Length; From 3f8b454ff4783b896516506b4ef192cda78134f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 10:01:28 +0900 Subject: [PATCH 237/387] Reword comment to match new filtering behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index e0d59e3b18..6d760df065 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Select.Carousel if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { - // bypass filtering for selected beatmap + // only check ruleset equality or convertability for selected beatmap Filtered.Value = !match; return; } From 04f1da04db8c685305b8688dd29d7d4af0985c6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 10:52:08 +0900 Subject: [PATCH 238/387] Remove incorrect xmldoc from SelectBeatmap function --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ca20b02bce..34d659cc90 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -201,9 +201,6 @@ namespace osu.Game.Screens.Select /// /// Selects a given beatmap on the carousel. - /// - /// If bypassFilters is false, we will try to select another unfiltered beatmap in the same set. If the - /// entire set is filtered, no selection is made. /// /// The beatmap to select. /// Whether to select the beatmap even if it is filtered (i.e., not visible on carousel). From ba0dec891d4a576f6158bff9235bc37cbb224f14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 10:58:36 +0900 Subject: [PATCH 239/387] Update test temporarily --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d16fd0bceb..f1ff08b92c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -609,7 +609,8 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); + // todo: this logic is changed in follow up PR. + AddAssert("Selected beatmap not changed", () => songSelect.Carousel.SelectedBeatmap != filteredBeatmap); } private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); From de9857ccdc1e5808c23ee0dc99c2c357c2f0984d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 11:00:09 +0900 Subject: [PATCH 240/387] Fix incorrect id reference in test --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 8295f0aa66..31c6e35492 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -638,7 +638,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.Beatmap.ID == 3); + .First(icon => icon.Item.Beatmap.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); From 5f8d180b5ec5acdff86933d7deeb5e0c3bcff6ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 11:51:26 +0900 Subject: [PATCH 241/387] Fix carousel scrolling being inoperable during beatmap import --- osu.Game/Screens/Select/BeatmapCarousel.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 04c08cdbd2..2dc063012f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -191,7 +191,9 @@ namespace osu.Game.Screens.Select root.AddChild(newSet); - applyActiveCriteria(false); + // only reset scroll position if already near the scroll target. + // without this, during a large beatmap import it is impossible to navigate the carousel. + applyActiveCriteria(false, alwaysResetScrollPosition: false); //check if we can/need to maintain our current selection. if (previouslySelectedID != null) @@ -411,7 +413,7 @@ namespace osu.Game.Screens.Select applyActiveCriteria(debounce); } - private void applyActiveCriteria(bool debounce) + private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) { if (root.Children.Any() != true) return; @@ -421,7 +423,9 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - scrollPositionCache.Invalidate(); + + if (alwaysResetScrollPosition || isAtScrollTarget) + ScrollToSelected(); } PendingFilter?.Cancel(); @@ -435,6 +439,9 @@ namespace osu.Game.Screens.Select private float? scrollTarget; + /// + /// Scroll to the current . + /// public void ScrollToSelected() => scrollPositionCache.Invalidate(); protected override bool OnKeyDown(KeyDownEvent e) @@ -601,7 +608,7 @@ namespace osu.Game.Screens.Select SelectionChanged?.Invoke(c.Beatmap); itemsCache.Invalidate(); - scrollPositionCache.Invalidate(); + ScrollToSelected(); } }; } @@ -688,6 +695,11 @@ namespace osu.Game.Screens.Select itemsCache.Validate(); } + /// + /// Denotes whether the current scroll position is roughly at the scroll target (the current selection). + /// + private bool isAtScrollTarget => scrollTarget != null && Precision.AlmostEquals(scrollTarget.Value, scroll.Current, 150); + private bool firstScroll = true; private void updateScrollPosition() From ac70fcc54469fe5edd2d6574f609169ac5dbdb0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 12:30:27 +0900 Subject: [PATCH 242/387] Change logic to be more resilient by identifying user scroll events --- osu.Game/Screens/Select/BeatmapCarousel.cs | 26 ++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2dc063012f..20f9bb1be8 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select ///
public bool BeatmapSetsLoaded { get; private set; } - private readonly OsuScrollContainer scroll; + private readonly CarouselScrollContainer scroll; private IEnumerable beatmapSets => root.Children.OfType(); @@ -424,7 +424,7 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - if (alwaysResetScrollPosition || isAtScrollTarget) + if (alwaysResetScrollPosition || !scroll.UserScrolling) ScrollToSelected(); } @@ -695,11 +695,6 @@ namespace osu.Game.Screens.Select itemsCache.Validate(); } - /// - /// Denotes whether the current scroll position is roughly at the scroll target (the current selection). - /// - private bool isAtScrollTarget => scrollTarget != null && Precision.AlmostEquals(scrollTarget.Value, scroll.Current, 150); - private bool firstScroll = true; private void updateScrollPosition() @@ -779,6 +774,23 @@ namespace osu.Game.Screens.Select { private bool rightMouseScrollBlocked; + /// + /// Whether the last scroll event was user triggered, directly on the scroll container. + /// + public bool UserScrolling { get; private set; } + + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) + { + UserScrolling = true; + base.OnUserScroll(value, animated, distanceDecay); + } + + public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null) + { + UserScrolling = false; + base.ScrollTo(value, animated, distanceDecay); + } + protected override bool OnMouseDown(MouseDownEvent e) { if (e.Button == MouseButton.Right) From c8cdc5fda5fe6050d0512a56d686e8ec9f28f0d5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Mar 2020 12:43:01 +0900 Subject: [PATCH 243/387] Expose half catcher width to movement skill --- .../Difficulty/CatchDifficultyCalculator.cs | 27 ++++++++++--------- .../Difficulty/Skills/Movement.cs | 7 +++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 44e1a8e5cc..a3315d36e8 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override int SectionLength => 750; + private float halfCatcherWidth; + public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { @@ -48,14 +50,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - float halfCatchWidth; - - using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - { - halfCatchWidth = catcher.CatchWidth * 0.5f; - halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. - } - CatchHitObject lastObject = null; // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. @@ -69,16 +63,25 @@ namespace osu.Game.Rulesets.Catch.Difficulty continue; if (lastObject != null) - yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatchWidth); + yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatcherWidth); lastObject = hitObject; } } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap) { - new Movement(), - }; + using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) + { + halfCatcherWidth = catcher.CatchWidth * 0.5f; + halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. + } + + return new Skill[] + { + new Movement(halfCatcherWidth), + }; + } protected override Mod[] DifficultyAdjustmentMods => new Mod[] { diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 7cd569035b..fd164907e0 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -20,9 +20,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills protected override double DecayWeight => 0.94; + protected readonly float HalfCatcherWidth; + private float? lastPlayerPosition; private float lastDistanceMoved; + public Movement(float halfCatcherWidth) + { + HalfCatcherWidth = halfCatcherWidth; + } + protected override double StrainValueOf(DifficultyHitObject current) { var catchCurrent = (CatchDifficultyHitObject)current; From 1733519c3a3f7e88cb6c96ecbe2041f4bf0dbb0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 12:59:30 +0900 Subject: [PATCH 244/387] Split out CatcherArea nested classes and reorder methods --- .../TestSceneCatcher.cs | 2 +- .../TestSceneDrawableHitObjects.cs | 2 +- .../TestSceneHyperDash.cs | 2 +- .../Beatmaps/CatchBeatmapProcessor.cs | 2 +- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 2 +- .../Replays/CatchAutoGenerator.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 460 ++++++++++++++ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 581 +----------------- osu.Game.Rulesets.Catch/UI/HitExplosion.cs | 122 ++++ 10 files changed, 605 insertions(+), 572 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/Catcher.cs create mode 100644 osu.Game.Rulesets.Catch/UI/HitExplosion.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index fbbe00bb6c..fe0d512166 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests [BackgroundDependencyLoader] private void load() { - SetContents(() => new CatcherArea.Catcher + SetContents(() => new Catcher { RelativePositionAxes = Axes.None, Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 304c7e3854..df5494aab0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public override IReadOnlyList RequiredTypes => new[] { - typeof(CatcherArea.Catcher), + typeof(Catcher), typeof(DrawableCatchRuleset), typeof(DrawableFruit), typeof(DrawableJuiceStream), diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 6f0d8f0a3a..49ff9df4d7 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests } } - private CatcherArea.Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; + private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 986dc9dbb9..7c81bcdf0c 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int thisDirection = nextObject.X > currentObject.X ? 1 : -1; double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); - float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext); + float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext); if (distanceToHyper < 0) { diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 44e1a8e5cc..8b7080b5bf 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { float halfCatchWidth; - using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) + using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) { halfCatchWidth = catcher.CatchWidth * 0.5f; halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index 4c72b9fd3e..1ef235f764 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition { - private readonly CatcherArea.Catcher catcher; + private readonly Catcher catcher; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 4649dcae90..b90b5812a6 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Replays public override Replay Generate() { // todo: add support for HT DT - const double dash_speed = CatcherArea.Catcher.BASE_SPEED; + const double dash_speed = Catcher.BASE_SPEED; const double movement_speed = dash_speed / 2; float lastPosition = 0.5f; double lastTime = 0; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs new file mode 100644 index 0000000000..2c8b080ee3 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -0,0 +1,460 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class Catcher : Container, IKeyBindingHandler + { + /// + /// Whether we are hyper-dashing or not. + /// + public bool HyperDashing => hyperDashModifier != 1; + + /// + /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. + /// + public const double BASE_SPEED = 1.0 / 512; + + public Container ExplodingFruitTarget; + + public Container AdditiveTarget; + + public CatcherAnimationState CurrentState { get; private set; } + + /// + /// Width of the area that can be used to attempt catches during gameplay. + /// + internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X); + + protected bool Dashing + { + get => dashing; + set + { + if (value == dashing) return; + + dashing = value; + + Trail |= dashing; + } + } + + /// + /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// + protected bool Trail + { + get => trail; + set + { + if (value == trail) return; + + trail = value; + + if (Trail) + beginTrail(); + } + } + + private Container caughtFruit; + + private CatcherSprite catcherIdle; + private CatcherSprite catcherKiai; + private CatcherSprite catcherFail; + + private int currentDirection; + + private bool dashing; + + private bool trail; + + private double hyperDashModifier = 1; + private int hyperDashDirection; + private float hyperDashTargetPosition; + + public Catcher(BeatmapDifficulty difficulty = null) + { + RelativePositionAxes = Axes.X; + X = 0.5f; + + Origin = Anchor.TopCentre; + + Size = new Vector2(CatcherArea.CATCHER_SIZE); + if (difficulty != null) + Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + caughtFruit = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherFail = new CatcherSprite(CatcherAnimationState.Fail) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + } + }; + + updateCatcher(); + } + + /// + /// Add a caught fruit to the catcher's stack. + /// + /// The fruit that was caught. + public void PlaceOnPlate(DrawableCatchHitObject fruit) + { + var ourRadius = fruit.DisplayRadius; + float theirRadius = 0; + + const float allowance = 6; + + while (caughtFruit.Any(f => + f.LifetimeEnd == double.MaxValue && + Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) + { + var diff = (ourRadius + theirRadius) / allowance; + fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; + fruit.Y -= RNG.NextSingle() * diff; + } + + fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); + + caughtFruit.Add(fruit); + + Add(new HitExplosion(fruit) + { + X = fruit.X, + Scale = new Vector2(fruit.HitObject.Scale) + }); + } + + /// + /// Let the catcher attempt to catch a fruit. + /// + /// The fruit to catch. + /// Whether the catch is possible. + public bool AttemptCatch(CatchHitObject fruit) + { + var halfCatchWidth = CatchWidth * 0.5f; + + // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. + var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; + var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; + + var validCatch = + catchObjectPosition >= catcherPosition - halfCatchWidth && + catchObjectPosition <= catcherPosition + halfCatchWidth; + + // only update hyperdash state if we are catching a fruit. + // exceptions are Droplets and JuiceStreams. + if (!(fruit is Fruit)) return validCatch; + + if (validCatch && fruit.HyperDash) + { + var target = fruit.HyperDashTarget; + var timeDifference = target.StartTime - fruit.StartTime; + double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; + var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); + + SetHyperDashState(Math.Abs(velocity), target.X); + } + else + SetHyperDashState(); + + if (validCatch) + updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); + else if (!(fruit is Banana)) + updateState(CatcherAnimationState.Fail); + + return validCatch; + } + + /// + /// Set hyper-dash state. + /// + /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state. + /// When this catcher crosses this position, this catcher ends hyper-dashing. + public void SetHyperDashState(double modifier = 1, float targetPosition = -1) + { + const float hyper_dash_transition_length = 180; + + var wasHyperDashing = HyperDashing; + + if (modifier <= 1 || X == targetPosition) + { + hyperDashModifier = 1; + hyperDashDirection = 0; + + if (wasHyperDashing) + { + this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); + this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); + Trail &= Dashing; + } + } + else + { + hyperDashModifier = modifier; + hyperDashDirection = Math.Sign(targetPosition - X); + hyperDashTargetPosition = targetPosition; + + if (!wasHyperDashing) + { + this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); + this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); + Trail = true; + + var hyperDashEndGlow = createAdditiveSprite(true); + + hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.FadeOut(1200); + hyperDashEndGlow.Expire(true); + } + } + } + + public bool OnPressed(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection--; + return true; + + case CatchAction.MoveRight: + currentDirection++; + return true; + + case CatchAction.Dash: + Dashing = true; + return true; + } + + return false; + } + + public void OnReleased(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection++; + break; + + case CatchAction.MoveRight: + currentDirection--; + break; + + case CatchAction.Dash: + Dashing = false; + break; + } + } + + public void UpdatePosition(float position) + { + position = Math.Clamp(position, 0, 1); + + if (position == X) + return; + + Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y); + X = position; + } + + /// + /// Drop any fruit off the plate. + /// + public void Drop() + { + foreach (var f in caughtFruit.ToArray()) + Drop(f); + } + + /// + /// Explode any fruit off the plate. + /// + public void Explode() + { + foreach (var f in caughtFruit.ToArray()) + Explode(f); + } + + public void Drop(DrawableHitObject fruit) + { + removeFromPlateWithTransform(fruit, f => + { + f.MoveToY(f.Y + 75, 750, Easing.InSine); + f.FadeOut(750); + }); + } + + public void Explode(DrawableHitObject fruit) + { + var originalX = fruit.X * Scale.X; + + removeFromPlateWithTransform(fruit, f => + { + f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine); + f.MoveToX(f.X + originalX * 6, 1000); + f.FadeOut(750); + }); + } + + protected override void Update() + { + base.Update(); + + if (currentDirection == 0) return; + + var direction = Math.Sign(currentDirection); + + var dashModifier = Dashing ? 1 : 0.5; + var speed = BASE_SPEED * dashModifier * hyperDashModifier; + + UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); + + // Correct overshooting. + if (hyperDashDirection > 0 && hyperDashTargetPosition < X || + hyperDashDirection < 0 && hyperDashTargetPosition > X) + { + X = hyperDashTargetPosition; + SetHyperDashState(); + } + } + + private void updateCatcher() + { + catcherIdle.Hide(); + catcherKiai.Hide(); + catcherFail.Hide(); + + CatcherSprite current; + + switch (CurrentState) + { + default: + current = catcherIdle; + break; + + case CatcherAnimationState.Fail: + current = catcherFail; + break; + + case CatcherAnimationState.Kiai: + current = catcherKiai; + break; + } + + current.Show(); + (current.Drawable as IAnimation)?.GotoFrame(0); + } + + private void beginTrail() + { + Trail &= dashing || HyperDashing; + Trail &= AdditiveTarget != null; + + if (!Trail) return; + + var additive = createAdditiveSprite(HyperDashing); + + additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); + additive.Expire(true); + + Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); + } + + private Drawable createAdditiveSprite(bool hyperDash) + { + var additive = createCatcherSprite(); + + additive.Anchor = Anchor; + additive.Scale = Scale; + additive.Colour = hyperDash ? Color4.Red : Color4.White; + additive.Blending = BlendingParameters.Additive; + additive.RelativePositionAxes = RelativePositionAxes; + additive.Position = Position; + + AdditiveTarget.Add(additive); + + return additive; + } + + private Drawable createCatcherSprite() + { + return new CatcherSprite(CurrentState); + } + + private void updateState(CatcherAnimationState state) + { + if (CurrentState == state) + return; + + CurrentState = state; + updateCatcher(); + } + + private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) + { + if (ExplodingFruitTarget != null) + { + fruit.Anchor = Anchor.TopLeft; + fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); + + if (!caughtFruit.Remove(fruit)) + // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). + // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. + return; + + ExplodingFruitTarget.Add(fruit); + } + + var actionTime = Clock.CurrentTime; + + fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState; + onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value); + + void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state) + { + using (fruit.BeginAbsoluteSequence(actionTime)) + action(fruit); + + fruit.Expire(); + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 2394110165..e0d9ff759d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -2,15 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Input.Bindings; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; @@ -20,7 +13,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { @@ -28,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI { public const float CATCHER_SIZE = 106.75f; - protected internal readonly Catcher MovableCatcher; - public Func> CreateDrawableRepresentation; public Container ExplodingFruitTarget @@ -37,6 +27,8 @@ namespace osu.Game.Rulesets.Catch.UI set => MovableCatcher.ExplodingFruitTarget = value; } + private DrawableCatchHitObject lastPlateableFruit; + public CatcherArea(BeatmapDifficulty difficulty = null) { RelativeSizeAxes = Axes.X; @@ -47,7 +39,10 @@ namespace osu.Game.Rulesets.Catch.UI }; } - private DrawableCatchHitObject lastPlateableFruit; + public static float GetCatcherSize(BeatmapDifficulty difficulty) + { + return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) { @@ -100,6 +95,15 @@ namespace osu.Game.Rulesets.Catch.UI } } + public void OnReleased(CatchAction action) + { + } + + public bool AttemptCatch(CatchHitObject obj) + { + return MovableCatcher.AttemptCatch(obj); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -110,559 +114,6 @@ namespace osu.Game.Rulesets.Catch.UI MovableCatcher.X = state.CatcherX.Value; } - public void OnReleased(CatchAction action) - { - } - - public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj); - - public static float GetCatcherSize(BeatmapDifficulty difficulty) - { - return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); - } - - public class Catcher : Container, IKeyBindingHandler - { - /// - /// Width of the area that can be used to attempt catches during gameplay. - /// - internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X); - - private Container caughtFruit; - - public Container ExplodingFruitTarget; - - public Container AdditiveTarget; - - public Catcher(BeatmapDifficulty difficulty = null) - { - RelativePositionAxes = Axes.X; - X = 0.5f; - - Origin = Anchor.TopCentre; - - Size = new Vector2(CATCHER_SIZE); - if (difficulty != null) - Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - caughtFruit = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - }, - catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - }, - catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - }, - catcherFail = new CatcherSprite(CatcherAnimationState.Fail) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - } - }; - - updateCatcher(); - } - - private CatcherSprite catcherIdle; - private CatcherSprite catcherKiai; - private CatcherSprite catcherFail; - - private void updateCatcher() - { - catcherIdle.Hide(); - catcherKiai.Hide(); - catcherFail.Hide(); - - CatcherSprite current; - - switch (CurrentState) - { - default: - current = catcherIdle; - break; - - case CatcherAnimationState.Fail: - current = catcherFail; - break; - - case CatcherAnimationState.Kiai: - current = catcherKiai; - break; - } - - current.Show(); - (current.Drawable as IAnimation)?.GotoFrame(0); - } - - private int currentDirection; - - private bool dashing; - - protected bool Dashing - { - get => dashing; - set - { - if (value == dashing) return; - - dashing = value; - - Trail |= dashing; - } - } - - private bool trail; - - /// - /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. - /// - protected bool Trail - { - get => trail; - set - { - if (value == trail) return; - - trail = value; - - if (Trail) - beginTrail(); - } - } - - private void beginTrail() - { - Trail &= dashing || HyperDashing; - Trail &= AdditiveTarget != null; - - if (!Trail) return; - - var additive = createAdditiveSprite(HyperDashing); - - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); - additive.Expire(true); - - Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); - } - - private Drawable createAdditiveSprite(bool hyperDash) - { - var additive = createCatcherSprite(); - - additive.Anchor = Anchor; - additive.Scale = Scale; - additive.Colour = hyperDash ? Color4.Red : Color4.White; - additive.Blending = BlendingParameters.Additive; - additive.RelativePositionAxes = RelativePositionAxes; - additive.Position = Position; - - AdditiveTarget.Add(additive); - - return additive; - } - - private Drawable createCatcherSprite() => new CatcherSprite(CurrentState); - - /// - /// Add a caught fruit to the catcher's stack. - /// - /// The fruit that was caught. - public void PlaceOnPlate(DrawableCatchHitObject fruit) - { - float ourRadius = fruit.DisplayRadius; - float theirRadius = 0; - - const float allowance = 6; - - while (caughtFruit.Any(f => - f.LifetimeEnd == double.MaxValue && - Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) - { - float diff = (ourRadius + theirRadius) / allowance; - fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; - fruit.Y -= RNG.NextSingle() * diff; - } - - fruit.X = Math.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2); - - caughtFruit.Add(fruit); - - Add(new HitExplosion(fruit) - { - X = fruit.X, - Scale = new Vector2(fruit.HitObject.Scale) - }); - } - - /// - /// Let the catcher attempt to catch a fruit. - /// - /// The fruit to catch. - /// Whether the catch is possible. - public bool AttemptCatch(CatchHitObject fruit) - { - float halfCatchWidth = CatchWidth * 0.5f; - - // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. - var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; - var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; - - var validCatch = - catchObjectPosition >= catcherPosition - halfCatchWidth && - catchObjectPosition <= catcherPosition + halfCatchWidth; - - // only update hyperdash state if we are catching a fruit. - // exceptions are Droplets and JuiceStreams. - if (!(fruit is Fruit)) return validCatch; - - if (validCatch && fruit.HyperDash) - { - var target = fruit.HyperDashTarget; - double timeDifference = target.StartTime - fruit.StartTime; - double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; - double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); - - SetHyperDashState(Math.Abs(velocity), target.X); - } - else - { - SetHyperDashState(); - } - - if (validCatch) - updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); - else if (!(fruit is Banana)) - updateState(CatcherAnimationState.Fail); - - return validCatch; - } - - private void updateState(CatcherAnimationState state) - { - if (CurrentState == state) - return; - - CurrentState = state; - updateCatcher(); - } - - public CatcherAnimationState CurrentState { get; private set; } - - private double hyperDashModifier = 1; - private int hyperDashDirection; - private float hyperDashTargetPosition; - - /// - /// Whether we are hyper-dashing or not. - /// - public bool HyperDashing => hyperDashModifier != 1; - - /// - /// Set hyper-dash state. - /// - /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state. - /// When this catcher crosses this position, this catcher ends hyper-dashing. - public void SetHyperDashState(double modifier = 1, float targetPosition = -1) - { - const float hyper_dash_transition_length = 180; - - bool wasHyperDashing = HyperDashing; - - if (modifier <= 1 || X == targetPosition) - { - hyperDashModifier = 1; - hyperDashDirection = 0; - - if (wasHyperDashing) - { - this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); - Trail &= Dashing; - } - } - else - { - hyperDashModifier = modifier; - hyperDashDirection = Math.Sign(targetPosition - X); - hyperDashTargetPosition = targetPosition; - - if (!wasHyperDashing) - { - this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); - Trail = true; - - var hyperDashEndGlow = createAdditiveSprite(true); - - hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); - hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); - hyperDashEndGlow.FadeOut(1200); - hyperDashEndGlow.Expire(true); - } - } - } - - public bool OnPressed(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection--; - return true; - - case CatchAction.MoveRight: - currentDirection++; - return true; - - case CatchAction.Dash: - Dashing = true; - return true; - } - - return false; - } - - public void OnReleased(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection++; - break; - - case CatchAction.MoveRight: - currentDirection--; - break; - - case CatchAction.Dash: - Dashing = false; - break; - } - } - - /// - /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. - /// - public const double BASE_SPEED = 1.0 / 512; - - protected override void Update() - { - base.Update(); - - if (currentDirection == 0) return; - - var direction = Math.Sign(currentDirection); - - double dashModifier = Dashing ? 1 : 0.5; - double speed = BASE_SPEED * dashModifier * hyperDashModifier; - - UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); - - // Correct overshooting. - if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || - (hyperDashDirection < 0 && hyperDashTargetPosition > X)) - { - X = hyperDashTargetPosition; - SetHyperDashState(); - } - } - - public void UpdatePosition(float position) - { - position = Math.Clamp(position, 0, 1); - - if (position == X) - return; - - Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y); - X = position; - } - - /// - /// Drop any fruit off the plate. - /// - public void Drop() - { - foreach (var f in caughtFruit.ToArray()) - Drop(f); - } - - /// - /// Explode any fruit off the plate. - /// - public void Explode() - { - foreach (var f in caughtFruit.ToArray()) - Explode(f); - } - - public void Drop(DrawableHitObject fruit) => removeFromPlateWithTransform(fruit, f => - { - f.MoveToY(f.Y + 75, 750, Easing.InSine); - f.FadeOut(750); - }); - - public void Explode(DrawableHitObject fruit) - { - var originalX = fruit.X * Scale.X; - - removeFromPlateWithTransform(fruit, f => - { - f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine); - f.MoveToX(f.X + originalX * 6, 1000); - f.FadeOut(750); - }); - } - - private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) - { - if (ExplodingFruitTarget != null) - { - fruit.Anchor = Anchor.TopLeft; - fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - - if (!caughtFruit.Remove(fruit)) - // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). - // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. - return; - - ExplodingFruitTarget.Add(fruit); - } - - double actionTime = Clock.CurrentTime; - - fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState; - onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value); - - void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state) - { - using (fruit.BeginAbsoluteSequence(actionTime)) - action(fruit); - - fruit.Expire(); - } - } - } - } - - public class HitExplosion : CompositeDrawable - { - private readonly CircularContainer largeFaint; - - public HitExplosion(DrawableCatchHitObject fruit) - { - Size = new Vector2(20); - Anchor = Anchor.TopCentre; - Origin = Anchor.BottomCentre; - - Color4 objectColour = fruit.AccentColour.Value; - - // scale roughly in-line with visual appearance of notes - - const float angle_variangle = 15; // should be less than 45 - - const float roundness = 100; - - const float initial_height = 10; - - var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); - - InternalChildren = new Drawable[] - { - largeFaint = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - // we want our size to be very small so the glow dominates it. - Size = new Vector2(0.8f), - Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), - Roundness = 160, - Radius = 200, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), - Roundness = 20, - Radius = 50, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.01f, initial_height), - Blending = BlendingParameters.Additive, - Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.01f, initial_height), - Blending = BlendingParameters.Additive, - Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - const double duration = 400; - - largeFaint - .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) - .FadeOut(duration * 2); - - this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); - Expire(true); - } + protected internal readonly Catcher MovableCatcher; } } diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs new file mode 100644 index 0000000000..04a86f83be --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -0,0 +1,122 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class HitExplosion : CompositeDrawable + { + private readonly CircularContainer largeFaint; + + public HitExplosion(DrawableCatchHitObject fruit) + { + Size = new Vector2(20); + Anchor = Anchor.TopCentre; + Origin = Anchor.BottomCentre; + + Color4 objectColour = fruit.AccentColour.Value; + + // scale roughly in-line with visual appearance of notes + + const float angle_variangle = 15; // should be less than 45 + + const float roundness = 100; + + const float initial_height = 10; + + var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); + + InternalChildren = new Drawable[] + { + largeFaint = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + // we want our size to be very small so the glow dominates it. + Size = new Vector2(0.8f), + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), + Roundness = 160, + Radius = 200, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), + Roundness = 20, + Radius = 50, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour, + Roundness = roundness, + Radius = 40, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour, + Roundness = roundness, + Radius = 40, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const double duration = 400; + + largeFaint + .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) + .FadeOut(duration * 2); + + this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); + Expire(true); + } + } +} From 4a774d02e0cf639c8a20b98eaea80f3cd26be71e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 14:43:27 +0900 Subject: [PATCH 245/387] Remove exo font loading --- osu.Game/OsuGameBase.cs | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b2277e2abf..93a845a6ae 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,30 +138,17 @@ namespace osu.Game dependencies.Cache(LocalConfig); AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Exo2.0-Medium"); - AddFont(Resources, @"Fonts/Exo2.0-MediumItalic"); - - AddFont(Resources, @"Fonts/Noto-Basic"); - AddFont(Resources, @"Fonts/Noto-Hangul"); - AddFont(Resources, @"Fonts/Noto-CJK-Basic"); - AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); - - AddFont(Resources, @"Fonts/Exo2.0-Regular"); - AddFont(Resources, @"Fonts/Exo2.0-RegularItalic"); - AddFont(Resources, @"Fonts/Exo2.0-SemiBold"); - AddFont(Resources, @"Fonts/Exo2.0-SemiBoldItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Bold"); - AddFont(Resources, @"Fonts/Exo2.0-BoldItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Light"); - AddFont(Resources, @"Fonts/Exo2.0-LightItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Black"); - AddFont(Resources, @"Fonts/Exo2.0-BlackItalic"); AddFont(Resources, @"Fonts/Torus-SemiBold"); AddFont(Resources, @"Fonts/Torus-Bold"); AddFont(Resources, @"Fonts/Torus-Regular"); AddFont(Resources, @"Fonts/Torus-Light"); + AddFont(Resources, @"Fonts/Noto-Basic"); + AddFont(Resources, @"Fonts/Noto-Hangul"); + AddFont(Resources, @"Fonts/Noto-CJK-Basic"); + AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); + AddFont(Resources, @"Fonts/Venera-Light"); AddFont(Resources, @"Fonts/Venera-Bold"); AddFont(Resources, @"Fonts/Venera-Black"); From ae112cf14f2f419e73ae9c4e75d9d542e72b1312 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 13:31:59 +0900 Subject: [PATCH 246/387] Reorder torus loading to provide regular as default --- osu.Game/OsuGameBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 93a845a6ae..3c7ab27651 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -139,10 +139,10 @@ namespace osu.Game AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Torus-SemiBold"); - AddFont(Resources, @"Fonts/Torus-Bold"); AddFont(Resources, @"Fonts/Torus-Regular"); AddFont(Resources, @"Fonts/Torus-Light"); + AddFont(Resources, @"Fonts/Torus-SemiBold"); + AddFont(Resources, @"Fonts/Torus-Bold"); AddFont(Resources, @"Fonts/Noto-Basic"); AddFont(Resources, @"Fonts/Noto-Hangul"); From 288470c3138713756b4f24fa1ef1e8f4ab63c20c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 13:32:16 +0900 Subject: [PATCH 247/387] Remove exo specification completely --- osu.Game/Graphics/OsuFont.cs | 6 +----- osu.Game/Graphics/UserInterface/OsuTabControl.cs | 2 +- osu.Game/Graphics/UserInterface/PageTabControl.cs | 2 +- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 2 +- osu.Game/Overlays/News/NewsArticleCover.cs | 6 +++--- osu.Game/Screens/Menu/Disclaimer.cs | 6 +++--- osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs | 2 +- osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs | 2 +- 8 files changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 841936d2c5..1b5a3199a2 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -30,7 +30,7 @@ namespace osu.Game.Graphics /// Whether the font is italic. /// Whether all characters should be spaced the same distance apart. /// The . - public static FontUsage GetFont(Typeface typeface = Typeface.Exo, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) + public static FontUsage GetFont(Typeface typeface = Typeface.Torus, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), italics, fixedWidth); /// @@ -42,9 +42,6 @@ namespace osu.Game.Graphics { switch (typeface) { - case Typeface.Exo: - return "Exo2.0"; - case Typeface.Venera: return "Venera"; @@ -96,7 +93,6 @@ namespace osu.Game.Graphics public enum Typeface { - Exo, Venera, Torus } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 6c883d9893..ca9f1330f9 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -173,7 +173,7 @@ namespace osu.Game.Graphics.UserInterface new HoverClickSounds() }; - Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); + Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } protected override void OnActivated() => fadeActive(); diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index ddcb626701..d05a08108a 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -78,7 +78,7 @@ namespace osu.Game.Graphics.UserInterface new HoverClickSounds() }; - Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); + Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString(); diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 2576900db8..a0b1b27ebf 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -132,7 +132,7 @@ namespace osu.Game.Overlays.AccountCreation usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); - emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Exo, weight: FontWeight.Bold)); + emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); passwordDescription.AddText("At least "); characterCheckText = passwordDescription.AddText("8 characters long"); diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs index f61b30b381..e381b629e4 100644 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.News Left = 25, Bottom = 50, }, - Font = OsuFont.GetFont(Typeface.Exo, 24, FontWeight.Bold), + Font = OsuFont.GetFont(Typeface.Torus, 24, FontWeight.Bold), Text = info.Title, }, new OsuSpriteText @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.News Left = 25, Bottom = 30, }, - Font = OsuFont.GetFont(Typeface.Exo, 16, FontWeight.Bold), + Font = OsuFont.GetFont(Typeface.Torus, 16, FontWeight.Bold), Text = "by " + info.Author } }; @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.News { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(Typeface.Exo, 12, FontWeight.Black, false, false), + Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Black, false, false), Text = date.ToString("d MMM yyy").ToUpper(), Margin = new MarginPadding { diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index ee8200321b..35091028ae 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -90,14 +90,14 @@ namespace osu.Game.Screens.Menu } }; - textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.Light)); - textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.SemiBold)); + textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Light)); + textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.SemiBold)); textFlow.NewParagraph(); static void format(SpriteText t) => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold); - textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Exo, 20, FontWeight.SemiBold)); + textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold)); textFlow.NewParagraph(); textFlow.NewParagraph(); diff --git a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs index a55db096af..4152a9a3b2 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components if (host.NewValue != null) { hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Exo, weight: FontWeight.Bold, italics: true)); + hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold, italics: true)); flagContainer.Child = new UpdateableFlag(host.NewValue.Country) { RelativeSizeAxes = Axes.Both }; } diff --git a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs index f8fb192b5c..0d31805774 100644 --- a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs +++ b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Multi.Ranking.Pages rankText.AddText($"#{index + 1} ", s => { - s.Font = s.Font.With(Typeface.Exo, weight: FontWeight.Bold); + s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold); s.Colour = colours.YellowDark; }); From c45f9cafd4384b3903952dfe74f6ca8b59dda587 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 13:33:55 +0900 Subject: [PATCH 248/387] Add medium -> regular fallback for torus --- osu.Game/Graphics/OsuFont.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 1b5a3199a2..076cc241ed 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -59,7 +59,13 @@ namespace osu.Game.Graphics /// The . /// The string representation of in the specified . public static string GetWeightString(Typeface typeface, FontWeight weight) - => GetWeightString(GetFamilyString(typeface), weight); + { + if (typeface == Typeface.Torus && weight == FontWeight.Medium) + // torus doesn't have a medium; fallback to regular. + weight = FontWeight.Regular; + + return GetWeightString(GetFamilyString(typeface), weight); + } /// /// Retrieves the string representation of a . From f7c036726a72271a8418acd0919dbbe932d2a086 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Mar 2020 13:52:40 +0900 Subject: [PATCH 249/387] Add beatmap loading timeout --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 4 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 98 ++++++++++++++++------------ 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 5f1f0d1e40..61bd962648 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.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 System; using System.Collections.Generic; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; @@ -60,8 +61,9 @@ namespace osu.Game.Beatmaps /// /// The to create a playable for. /// The s to apply to the . + /// The loading timeout. /// The converted . /// If could not be converted to . - IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null); + IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 1e1ffad81e..bdcfc058b4 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -83,55 +83,73 @@ namespace osu.Game.Beatmaps /// The applicable . protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap); - public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null) + public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) { - mods ??= Array.Empty(); - - var rulesetInstance = ruleset.CreateInstance(); - - IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); - - // Check if the beatmap can be converted - if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert()) - throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter})."); - - // Apply conversion mods - foreach (var mod in mods.OfType()) - mod.ApplyToBeatmapConverter(converter); - - // Convert - IBeatmap converted = converter.Convert(); - - // Apply difficulty mods - if (mods.Any(m => m is IApplicableToDifficulty)) + using (var cancellationSource = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10))) { - converted.BeatmapInfo = converted.BeatmapInfo.Clone(); - converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); + mods ??= Array.Empty(); - foreach (var mod in mods.OfType()) - mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); - } + var rulesetInstance = ruleset.CreateInstance(); - IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted); + IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); - processor?.PreProcess(); + // Check if the beatmap can be converted + if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert()) + throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter})."); - // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed - foreach (var obj in converted.HitObjects) - obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + // Apply conversion mods + foreach (var mod in mods.OfType()) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + mod.ApplyToBeatmapConverter(converter); + } - foreach (var mod in mods.OfType()) - { + // Convert + IBeatmap converted = converter.Convert(); + + // Apply difficulty mods + if (mods.Any(m => m is IApplicableToDifficulty)) + { + converted.BeatmapInfo = converted.BeatmapInfo.Clone(); + converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); + + foreach (var mod in mods.OfType()) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); + } + } + + IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted); + + processor?.PreProcess(); + + // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed foreach (var obj in converted.HitObjects) - mod.ApplyToHitObject(obj); + { + cancellationSource.Token.ThrowIfCancellationRequested(); + obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + } + + foreach (var mod in mods.OfType()) + { + foreach (var obj in converted.HitObjects) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + mod.ApplyToHitObject(obj); + } + } + + processor?.PostProcess(); + + foreach (var mod in mods.OfType()) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + mod.ApplyToBeatmap(converted); + } + + return converted; } - - processor?.PostProcess(); - - foreach (var mod in mods.OfType()) - mod.ApplyToBeatmap(converted); - - return converted; } private CancellationTokenSource loadCancellation = new CancellationTokenSource(); From ef0acde458c7b9bea5fa42fc60e951fa6e3581fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 14:12:56 +0900 Subject: [PATCH 250/387] Adjust to allow for extra row --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 1ef2916ca1..fad9f00dc8 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -254,7 +254,7 @@ namespace osu.Game.Tournament.Screens.MapPool } } - if (totalRows > 8) + if (totalRows > 9) // remove horizontal padding to increase flow width to 3 panels mapFlows.Padding = new MarginPadding(5); } From fbb7e9f12a3971a6f3afb46138ba2f67b499d54b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 15:34:52 +0900 Subject: [PATCH 251/387] Add tests (wip) --- osu.Game.Tournament.Tests/LadderTestScene.cs | 122 ++++++++++++++++++ .../Screens/TestSceneMapPoolScreen.cs | 83 +++++++++++- .../Screens/TestSceneSeedingEditorScreen.cs | 2 +- .../Screens/TestSceneSeedingScreen.cs | 101 --------------- .../Screens/MapPool/MapPoolScreen.cs | 4 +- 5 files changed, 201 insertions(+), 111 deletions(-) diff --git a/osu.Game.Tournament.Tests/LadderTestScene.cs b/osu.Game.Tournament.Tests/LadderTestScene.cs index dae0721023..4477ca8338 100644 --- a/osu.Game.Tournament.Tests/LadderTestScene.cs +++ b/osu.Game.Tournament.Tests/LadderTestScene.cs @@ -3,7 +3,10 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Tournament.Models; +using osu.Game.Users; namespace osu.Game.Tournament.Tests { @@ -12,5 +15,124 @@ namespace osu.Game.Tournament.Tests { [Resolved] protected LadderInfo Ladder { get; private set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + TournamentMatch match = CreateSampleMatch(); + + Ladder.Rounds.Clear(); + Ladder.Rounds.Add(match.Round.Value); + + Ladder.Matches.Clear(); + Ladder.Matches.Add(match); + + Ladder.Teams.Clear(); + Ladder.Teams.Add(match.Team1.Value); + Ladder.Teams.Add(match.Team2.Value); + + Ladder.CurrentMatch.Value = match; + } + + public static TournamentMatch CreateSampleMatch() => new TournamentMatch + { + Team1 = + { + Value = new TournamentTeam + { + FlagName = { Value = "JP" }, + FullName = { Value = "Japan" }, + LastYearPlacing = { Value = 10 }, + Seed = { Value = "Low" }, + SeedingResults = + { + new SeedingResult + { + Mod = { Value = "NM" }, + Seed = { Value = 10 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 12345672, + Seed = { Value = 24 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 1234567, + Seed = { Value = 12 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 1234567, + Seed = { Value = 16 }, + } + } + }, + new SeedingResult + { + Mod = { Value = "DT" }, + Seed = { Value = 5 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 3 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 6 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 12 }, + } + } + } + }, + Players = + { + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, + } + } + }, + Team2 = + { + Value = new TournamentTeam + { + FlagName = { Value = "US" }, + FullName = { Value = "United States" }, + Players = + { + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + } + } + }, + Round = + { + Value = new TournamentRound { Name = { Value = "Quarterfinals" } } + } + }; + + public static BeatmapInfo CreateSampleBeatmapInfo() => + new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } }; } } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index a7011c6d3c..a73e4e53ba 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -1,24 +1,93 @@ // 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 System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.MapPool; namespace osu.Game.Tournament.Tests.Screens { public class TestSceneMapPoolScreen : LadderTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(MapPoolScreen) - }; + private MapPoolScreen screen; [BackgroundDependencyLoader] private void load() { - Add(new MapPoolScreen { Width = 0.7f }); + Add(screen = new MapPoolScreen { Width = 0.7f }); + } + + [Test] + public void TestFewMaps() + { + AddStep("load few maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 8; i++) + addBeatmap(); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + } + + [Test] + public void TestManyMaps() + { + AddStep("load many maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 17; i++) + addBeatmap(); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); + } + + [Test] + public void TestManyMods() + { + AddStep("load many maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 13; i++) + addBeatmap(i < 4 ? $"M{i}" : "NM"); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); + } + + private void addBeatmap(string mods = "nm") + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Mods = mods + }); } } } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs index 014cd4663b..17cccd34b6 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.Screens public TestSceneSeedingEditorScreen() { - var match = TestSceneSeedingScreen.CreateSampleSeededMatch(); + var match = CreateSampleMatch(); Add(new SeedingEditorScreen(match.Team1.Value) { diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs index 335a6c80a1..4269f8f56a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs @@ -3,10 +3,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.TeamIntro; -using osu.Game.Users; namespace osu.Game.Tournament.Tests.Screens { @@ -18,110 +16,11 @@ namespace osu.Game.Tournament.Tests.Screens [BackgroundDependencyLoader] private void load() { - ladder.CurrentMatch.Value = CreateSampleSeededMatch(); - Add(new SeedingScreen { FillMode = FillMode.Fit, FillAspectRatio = 16 / 9f }); } - - public static TournamentMatch CreateSampleSeededMatch() => new TournamentMatch - { - Team1 = - { - Value = new TournamentTeam - { - FlagName = { Value = "JP" }, - FullName = { Value = "Japan" }, - LastYearPlacing = { Value = 10 }, - Seed = { Value = "Low" }, - SeedingResults = - { - new SeedingResult - { - Mod = { Value = "NM" }, - Seed = { Value = 10 }, - Beatmaps = - { - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 12345672, - Seed = { Value = 24 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 1234567, - Seed = { Value = 12 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 1234567, - Seed = { Value = 16 }, - } - } - }, - new SeedingResult - { - Mod = { Value = "DT" }, - Seed = { Value = 5 }, - Beatmaps = - { - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 3 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 6 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 12 }, - } - } - } - }, - Players = - { - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, - } - } - }, - Team2 = - { - Value = new TournamentTeam - { - FlagName = { Value = "US" }, - FullName = { Value = "United States" }, - Players = - { - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - } - } - }, - Round = - { - Value = new TournamentRound { Name = { Value = "Quarterfinals" } } - } - }; } } diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index fad9f00dc8..a9e1d1f226 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -52,7 +52,6 @@ namespace osu.Game.Tournament.Screens.MapPool { Y = 100, Spacing = new Vector2(10, 10), - Padding = new MarginPadding(5) { Horizontal = 100 }, Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -212,6 +211,7 @@ namespace osu.Game.Tournament.Screens.MapPool private void matchChanged(ValueChangedEvent match) { mapFlows.Clear(); + mapFlows.Padding = new MarginPadding(5) { Horizontal = 100 }; int totalRows = 0; @@ -243,7 +243,7 @@ namespace osu.Game.Tournament.Screens.MapPool if (++flowCount > 2) { totalRows++; - flowCount = 0; + flowCount = 1; } currentFlow.Add(new TournamentBeatmapPanel(b.BeatmapInfo, b.Mods) From 00d7dc19cc4932ac1af3ca1c6da315dbcd8b5ee1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 14:25:25 +0900 Subject: [PATCH 252/387] Update tests and logic --- .../Screens/TestSceneMapPoolScreen.cs | 46 +++++++++++++++++-- .../Screens/MapPool/MapPoolScreen.cs | 7 +-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index a73e4e53ba..cc5f66761e 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -41,6 +41,26 @@ namespace osu.Game.Tournament.Tests.Screens AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); } + [Test] + public void TestJustEnoughMaps() + { + AddStep("load just enough maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 18; i++) + addBeatmap(); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + } + [Test] public void TestManyMaps() { @@ -48,7 +68,7 @@ namespace osu.Game.Tournament.Tests.Screens { Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); - for (int i = 0; i < 17; i++) + for (int i = 0; i < 19; i++) addBeatmap(); }); @@ -61,6 +81,26 @@ namespace osu.Game.Tournament.Tests.Screens AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); } + [Test] + public void TestJustEnoughMods() + { + AddStep("load many maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 11; i++) + addBeatmap(i > 4 ? $"M{i}" : "NM"); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + } + [Test] public void TestManyMods() { @@ -68,8 +108,8 @@ namespace osu.Game.Tournament.Tests.Screens { Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); - for (int i = 0; i < 13; i++) - addBeatmap(i < 4 ? $"M{i}" : "NM"); + for (int i = 0; i < 12; i++) + addBeatmap(i > 4 ? $"M{i}" : "NM"); }); AddStep("reset match", () => diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index a9e1d1f226..da7e226103 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -211,7 +211,6 @@ namespace osu.Game.Tournament.Screens.MapPool private void matchChanged(ValueChangedEvent match) { mapFlows.Clear(); - mapFlows.Padding = new MarginPadding(5) { Horizontal = 100 }; int totalRows = 0; @@ -254,9 +253,11 @@ namespace osu.Game.Tournament.Screens.MapPool } } - if (totalRows > 9) + mapFlows.Padding = new MarginPadding(5) + { // remove horizontal padding to increase flow width to 3 panels - mapFlows.Padding = new MarginPadding(5); + Horizontal = totalRows > 9 ? 0 : 100 + }; } } } From c33ca6e99c60e1fd4bbc1d321a229a55f3e99de5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Mar 2020 14:28:11 +0900 Subject: [PATCH 253/387] Decorate usages with exception management --- osu.Game/Screens/Edit/Editor.cs | 14 +++++++++- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 30 +++++++++++++-------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3a6f02f811..cf13f8a3a1 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -23,6 +23,7 @@ using osuTK.Input; using System.Collections.Generic; using osu.Framework; using osu.Framework.Input.Bindings; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; @@ -86,7 +87,18 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + try + { + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + } + catch (Exception e) + { + Logger.Error(e, "Could not load beatmap sucessfully!"); + //couldn't load, hard abort! + this.Exit(); + return; + } + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); dependencies.CacheAs(editorBeatmap); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index f84aac3081..59d2aca17d 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -23,6 +23,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -311,20 +312,27 @@ namespace osu.Game.Screens.Select Content = getBPMRange(b), })); - IBeatmap playableBeatmap; - try { - // Try to get the beatmap with the user's ruleset - playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty()); - } - catch (BeatmapInvalidForRulesetException) - { - // Can't be converted to the user's ruleset, so use the beatmap's own ruleset - playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty()); - } + IBeatmap playableBeatmap; - labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s))); + try + { + // Try to get the beatmap with the user's ruleset + playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty()); + } + catch (BeatmapInvalidForRulesetException) + { + // Can't be converted to the user's ruleset, so use the beatmap's own ruleset + playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty()); + } + + labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s))); + } + catch (Exception e) + { + Logger.Error(e, "Could not load beatmap sucessfully!"); + } } return labels.ToArray(); From 30ad580993b60944d395a3650214f53a8e6b75fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 14:09:09 +0900 Subject: [PATCH 254/387] Fix map pool screen vertical layout --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index da7e226103..7217629860 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader(), mapFlows = new FillFlowContainer> { - Y = 100, + Y = 160, Spacing = new Vector2(10, 10), Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, From edd444ea73fdc0df668b2e2c11f8fa15da1ac438 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 14:36:46 +0900 Subject: [PATCH 255/387] Fix mod sprite bleeding border colour --- .../Components/TournamentBeatmapPanel.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 4116ffbec6..dc4c4b9ec6 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -125,13 +125,21 @@ namespace osu.Game.Tournament.Components if (!string.IsNullOrEmpty(mods)) { - AddInternal(new Sprite + AddInternal(new Container { - Texture = textures.Get($"mods/{mods}"), + RelativeSizeAxes = Axes.Y, + Width = 60, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding(10), - Scale = new Vector2(0.8f) + Child = new Sprite + { + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Texture = textures.Get($"mods/{mods}"), + } }); } } From b902e5039636ec42a975e3b355f8b1f5dddad871 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 15:44:13 +0900 Subject: [PATCH 256/387] Add resolution selector in tournament setup screen --- .../Components/ControlPanel.cs | 6 ++-- osu.Game.Tournament/Screens/SetupScreen.cs | 29 ++++++++++++++++++- osu.Game.Tournament/TournamentGameBase.cs | 2 +- osu.Game.Tournament/TournamentSceneManager.cs | 18 ++++++++---- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tournament/Components/ControlPanel.cs b/osu.Game.Tournament/Components/ControlPanel.cs index fa5c941f1a..ef8c8767e0 100644 --- a/osu.Game.Tournament/Components/ControlPanel.cs +++ b/osu.Game.Tournament/Components/ControlPanel.cs @@ -22,9 +22,9 @@ namespace osu.Game.Tournament.Components public ControlPanel() { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.Y; AlwaysPresent = true; - Width = 0.15f; + Width = TournamentSceneManager.CONTROL_AREA_WIDTH; Anchor = Anchor.TopRight; InternalChildren = new Drawable[] @@ -47,8 +47,8 @@ namespace osu.Game.Tournament.Components Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Width = 0.75f, Position = new Vector2(0, 35f), + Padding = new MarginPadding(5), Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5f), }, diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 023582166c..b7f8b2bfd6 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.Drawing; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -22,6 +25,7 @@ namespace osu.Game.Tournament.Screens private FillFlowContainer fillFlow; private LoginOverlay loginOverlay; + private ActionableInfo resolution; [Resolved] private MatchIPCInfo ipc { get; set; } @@ -32,9 +36,13 @@ namespace osu.Game.Tournament.Screens [Resolved] private RulesetStore rulesets { get; set; } + private Bindable windowSize; + [BackgroundDependencyLoader] - private void load() + private void load(FrameworkConfigManager frameworkConfig) { + windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); + InternalChild = fillFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -48,6 +56,9 @@ namespace osu.Game.Tournament.Screens reload(); } + [Resolved] + private Framework.Game game { get; set; } + private void reload() { var fileBasedIpc = ipc as FileBasedIPC; @@ -97,9 +108,25 @@ namespace osu.Game.Tournament.Screens Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, + resolution = new ActionableInfo + { + Label = "Stream area resolution", + ButtonText = "Set to 1080p", + Action = () => + { + windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080); + } + } }; } + protected override void Update() + { + base.Update(); + + resolution.Value = $"{ScreenSpaceDrawQuad.Width:N0}x{ScreenSpaceDrawQuad.Height:N0}"; + } + public class LabelledDropdown : LabelledComponent, T> { public LabelledDropdown() diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 41822ae2c3..85db9e61fb 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -65,7 +65,7 @@ namespace osu.Game.Tournament windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => { - var minWidth = (int)(size.NewValue.Height / 9f * 16 + 400); + var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1; heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; }), true); diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index ef8d16011d..23fcb01db7 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -33,6 +33,12 @@ namespace osu.Game.Tournament private Container screens; private TourneyVideo video; + public const float CONTROL_AREA_WIDTH = 160; + + public const float STREAM_AREA_WIDTH = 1366; + + public const double REQUIRED_WIDTH = TournamentSceneManager.CONTROL_AREA_WIDTH * 2 + TournamentSceneManager.STREAM_AREA_WIDTH; + [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); @@ -51,13 +57,13 @@ namespace osu.Game.Tournament { new Container { - RelativeSizeAxes = Axes.Both, - X = 200, + RelativeSizeAxes = Axes.Y, + X = CONTROL_AREA_WIDTH, FillMode = FillMode.Fit, FillAspectRatio = 16 / 9f, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Size = new Vector2(0.8f, 1), + Width = STREAM_AREA_WIDTH, //Masking = true, Children = new Drawable[] { @@ -96,7 +102,7 @@ namespace osu.Game.Tournament new Container { RelativeSizeAxes = Axes.Y, - Width = 200, + Width = CONTROL_AREA_WIDTH, Children = new Drawable[] { new Box @@ -108,8 +114,8 @@ namespace osu.Game.Tournament { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - Padding = new MarginPadding(2), + Spacing = new Vector2(5), + Padding = new MarginPadding(5), Children = new Drawable[] { new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen }, From bee855bd1d914fd17cd108fb9fb55f8b6f1519fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 15:54:46 +0900 Subject: [PATCH 257/387] Remove using --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index dc4c4b9ec6..477bf4bd63 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -16,7 +16,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Tournament.Models; -using osuTK; using osuTK.Graphics; namespace osu.Game.Tournament.Components From 7e9d28b1b13120372a7afd6599d7d905817043b8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 13 Mar 2020 13:42:33 +0300 Subject: [PATCH 258/387] Fix slider ball colour affects follow circle --- .../Objects/Drawables/Pieces/SliderBall.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index c871089acd..287a2d7e92 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -23,9 +23,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public Func GetInitialHitAction; + public new Color4 Colour + { + get => ball.Colour; + set => ball.Colour = value; + } + private readonly Slider slider; private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; + private readonly CircularContainer ball; public SliderBall(Slider slider, DrawableSlider drawableSlider = null) { @@ -47,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Alpha = 0, Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), }, - new CircularContainer + ball = new CircularContainer { Masking = true, RelativeSizeAxes = Axes.Both, From 097bd37e37c38095113bd706717a9601afc1f3c0 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:27:33 +0100 Subject: [PATCH 259/387] Fix SelectorTab crashing tests after a reload For some reason, the default channel type (Public) caused the channel manager to attempt to connect to an API, which was null at that time, after hot reloading the test environment (via dynamic compilation). Changing the channel type seems to fix that. --- osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs index d5d9a6c2ce..5fb56a3f75 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs @@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.Tabs public ChannelSelectorTabChannel() { Name = "+"; + Type = ChannelType.Temporary; } } } From 8991e88039b0af4e39dd7122588542f7a916ccf8 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:33:06 +0100 Subject: [PATCH 260/387] Fix active tab closing behaviour --- .../Overlays/Chat/Tabs/ChannelTabControl.cs | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index 104495ae01..a72f182450 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Chat.Tabs // performTabSort might've made selectorTab's position wonky, fix it TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); - ((ChannelTabItem)item).OnRequestClose += tabCloseRequested; + ((ChannelTabItem)item).OnRequestClose += channelItem => OnRequestLeave?.Invoke(channelItem.Value); base.AddTabItem(item, addToDropdown); } @@ -74,18 +74,24 @@ namespace osu.Game.Overlays.Chat.Tabs /// /// Removes a channel from the ChannelTabControl. - /// If the selected channel is the one that is beeing removed, the next available channel will be selected. + /// If the selected channel is the one that is being removed, the next available channel will be selected. /// /// The channel that is going to be removed. public void RemoveChannel(Channel channel) { - RemoveItem(channel); - if (Current.Value == channel) { - // Prefer non-selector channels first - Current.Value = Items.FirstOrDefault(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)) ?? Items.FirstOrDefault(); + var allChannels = TabContainer.AllTabItems.Select(tab => tab.Value).ToList(); + var isNextTabSelector = allChannels[allChannels.IndexOf(channel) + 1] == selectorTab.Value; + + // selectorTab is not switchable, so we have to explicitly select it if it's the only tab left + if (isNextTabSelector && allChannels.Count == 2) + SelectTab(selectorTab); + else + SwitchTab(isNextTabSelector ? -1 : 1); } + + RemoveItem(channel); } protected override void SelectTab(TabItem tab) @@ -100,21 +106,6 @@ namespace osu.Game.Overlays.Chat.Tabs selectorTab.Active.Value = false; } - private void tabCloseRequested(TabItem tab) - { - int totalTabs = TabContainer.Count - 1; // account for selectorTab - int currentIndex = Math.Clamp(TabContainer.IndexOf(tab), 1, totalTabs); - - if (tab == SelectedTab && totalTabs > 1) - // Select the tab after tab-to-be-removed's index, or the tab before if current == last - SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]); - else if (totalTabs == 1 && !selectorTab.Active.Value) - // Open channel selection overlay if all channel tabs will be closed after removing this tab - SelectTab(selectorTab); - - OnRequestLeave?.Invoke(tab.Value); - } - protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer { Direction = FillDirection.Full, From 694e56b0d13cc90e0f82b1f14661dd06fbf03f8d Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:33:58 +0100 Subject: [PATCH 261/387] Add non-PM chat tabs to tests --- .../Visual/Online/TestSceneChatOverlay.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 19bdaff6ff..8aa30c7fa3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -196,12 +196,22 @@ namespace osu.Game.Tests.Visual.Online private class TestTabControl : ChannelTabControl { - protected override TabItem CreateTabItem(Channel value) => new TestChannelTabItem(value); + protected override TabItem CreateTabItem(Channel value) + { + switch (value.Type) + { + case ChannelType.PM: + return new TestPrivateChannelTabItem(value); + + default: + return new TestChannelTabItem(value); + } + } public new IReadOnlyDictionary> TabMap => base.TabMap; } - private class TestChannelTabItem : PrivateChannelTabItem + private class TestChannelTabItem : ChannelTabItem { public TestChannelTabItem(Channel channel) : base(channel) @@ -210,5 +220,15 @@ namespace osu.Game.Tests.Visual.Online public new ClickableContainer CloseButton => base.CloseButton; } + + private class TestPrivateChannelTabItem : PrivateChannelTabItem + { + public TestPrivateChannelTabItem(Channel channel) + : base(channel) + { + } + + public new ClickableContainer CloseButton => base.CloseButton; + } } } From 0bbae094ddf854d6e9d6ee3c3197df0577071231 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:34:22 +0100 Subject: [PATCH 262/387] Add active tab closing behaviour tests --- .../Visual/Online/TestSceneChatOverlay.cs | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 8aa30c7fa3..ede99c06be 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -38,8 +38,13 @@ namespace osu.Game.Tests.Visual.Online private TestChatOverlay chatOverlay; private ChannelManager channelManager; + private IEnumerable visibleChannels => chatOverlay.ChannelTabControl.VisibleItems.Where(channel => channel.Name != "+"); + private IEnumerable joinedChannels => chatOverlay.ChannelTabControl.Items.Where(channel => channel.Name != "+"); private readonly List channels; + private Channel currentChannel => channelManager.CurrentChannel.Value; + private Channel nextChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) + 1); + private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); private Channel channel1 => channels[0]; private Channel channel2 => channels[1]; @@ -91,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - AddAssert("Current channel is channel 1", () => channelManager.CurrentChannel.Value == channel1); + AddAssert("Current channel is channel 1", () => currentChannel == channel1); AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); } @@ -102,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); - AddAssert("Current channel is channel 1", () => channelManager.CurrentChannel.Value == channel1); + AddAssert("Current channel is channel 1", () => currentChannel == channel1); - AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); } @@ -140,10 +145,67 @@ namespace osu.Game.Tests.Visual.Online var targetNumberKey = oneBasedIndex % 10; var targetChannel = channels[zeroBasedIndex]; AddStep($"press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); - AddAssert($"channel #{oneBasedIndex} is selected", () => channelManager.CurrentChannel.Value == targetChannel); + AddAssert($"channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel); } } + private Channel expectedChannel; + + [Test] + public void TestCloseChannelWhileActive() + { + AddUntilStep("Join until dropdown has channels", () => + { + if (visibleChannels.Count() < joinedChannels.Count()) + return true; + + // Using temporary channels because they don't hide their names when not active + Channel toAdd = new Channel { Name = $"test channel {joinedChannels.Count()}", Type = ChannelType.Temporary }; + channelManager.JoinChannel(toAdd); + + return false; + }); + + AddStep("Switch to last tab", () => clickDrawable(chatOverlay.TabMap[visibleChannels.Last()])); + AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); + + // Closing the last channel before dropdown + AddStep("Close current channel", () => + { + expectedChannel = nextChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Next channel selected", () => currentChannel == expectedChannel); + + // Depending on the window size, one more channel might need to be closed for the selectorTab to appear + AddUntilStep("Close channels until selector visible", () => + { + if (chatOverlay.ChannelTabControl.VisibleItems.Last().Name == "+") + return true; + + chatOverlay.ChannelTabControl.RemoveChannel(visibleChannels.Last()); + return false; + }); + AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); + + // Closing the last channel with dropdown no longer present + AddStep("Close last when selector next", () => + { + expectedChannel = previousChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Channel changed to previous", () => currentChannel == expectedChannel); + + // Standard channel closing + AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1)); + AddStep("Close current channel", () => + { + expectedChannel = nextChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Channel changed to next", () => currentChannel == expectedChannel); + } + private void pressChannelHotkey(int number) { var channelKey = Key.Number0 + number; @@ -187,6 +249,8 @@ namespace osu.Game.Tests.Visual.Online { public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value; + public new ChannelTabControl ChannelTabControl => base.ChannelTabControl; + public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay; protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl(); From 8d3cab0e1696e62c3c018cab91a1ee797b8bcc03 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:58:32 +0100 Subject: [PATCH 263/387] Trim whitespace --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index ede99c06be..297a51c4a5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -107,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddStep("Close channel 2", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); AddAssert("Current channel is channel 1", () => currentChannel == channel1); - AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); } From 38d00c7f0ae6abb0283a366fadfca6cabb048948 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 21:29:10 +0100 Subject: [PATCH 264/387] Revert unnecessary changes and actually trim the whitespace --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 297a51c4a5..736bfd8e7d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -107,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); AddAssert("Current channel is channel 1", () => currentChannel == channel1); - AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); } @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); // Closing the last channel with dropdown no longer present - AddStep("Close last when selector next", () => + AddStep("Close last when selector next", () => { expectedChannel = previousChannel; chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Online // Standard channel closing AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1)); - AddStep("Close current channel", () => + AddStep("Close current channel", () => { expectedChannel = nextChannel; chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); From 202c8cdad8ef0cf223b48f6a0590622004fa2efd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Mar 2020 15:35:59 +0900 Subject: [PATCH 265/387] Add braces to satisfy codefactor --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 2c8b080ee3..a3dc58bc19 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -348,8 +348,8 @@ namespace osu.Game.Rulesets.Catch.UI UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); // Correct overshooting. - if (hyperDashDirection > 0 && hyperDashTargetPosition < X || - hyperDashDirection < 0 && hyperDashTargetPosition > X) + if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || + (hyperDashDirection < 0 && hyperDashTargetPosition > X)) { X = hyperDashTargetPosition; SetHyperDashState(); From cd0f1c98ba710cfd29130859918e5b31657a7f62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Mar 2020 16:17:14 +0900 Subject: [PATCH 266/387] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f623a92ade..d7a76c97af 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba6f0e2251..882dd82a53 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 54cd400d51..7872db676b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 45dfb22bd50ae25884e2b159d27747acd8a5819d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Mar 2020 16:25:10 +0900 Subject: [PATCH 267/387] Centralise additive texture creation --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 39 +++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 13f1ddf1d7..2caae8c0c7 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -251,22 +251,10 @@ namespace osu.Game.Rulesets.Catch.UI return; } - Texture tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; - - var additive = new CatcherTrailSprite(tex) - { - Anchor = Anchor, - Scale = Scale, - Colour = HyperDashing ? Color4.Red : Color4.White, - Blending = BlendingParameters.Additive, - RelativePositionAxes = RelativePositionAxes, - Position = Position - }; + var additive = createAdditiveSprite(); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); additive.Expire(true); - - AdditiveTarget?.Add(additive); Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } @@ -400,10 +388,10 @@ namespace osu.Game.Rulesets.Catch.UI this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); Trail = true; - var hyperDashEndGlow = createAdditiveSprite(true); + var hyperDashEndGlow = createAdditiveSprite(); - hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); - hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); hyperDashEndGlow.FadeOut(1200); hyperDashEndGlow.Expire(true); } @@ -522,6 +510,25 @@ namespace osu.Game.Rulesets.Catch.UI }); } + private CatcherTrailSprite createAdditiveSprite() + { + Texture tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; + + var sprite = new CatcherTrailSprite(tex) + { + Anchor = Anchor, + Scale = Scale, + Colour = HyperDashing ? Color4.Red : Color4.White, + Blending = BlendingParameters.Additive, + RelativePositionAxes = RelativePositionAxes, + Position = Position + }; + + AdditiveTarget?.Add(sprite); + + return sprite; + } + private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) { if (ExplodingFruitTarget != null) From d3f23b766e775e068199487c9859091b5c395eb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Mar 2020 17:06:23 +0900 Subject: [PATCH 268/387] Move across to new file in line with master --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 59 ++++++++++++++++++--------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index a3dc58bc19..29bed00d61 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -55,14 +56,14 @@ namespace osu.Game.Rulesets.Catch.UI } /// - /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// Activate or deactivate the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. /// protected bool Trail { get => trail; set { - if (value == trail) return; + if (value == trail || AdditiveTarget == null) return; trail = value; @@ -77,6 +78,8 @@ namespace osu.Game.Rulesets.Catch.UI private CatcherSprite catcherKiai; private CatcherSprite catcherFail; + private CatcherSprite currentCatcher; + private int currentDirection; private bool dashing; @@ -236,10 +239,10 @@ namespace osu.Game.Rulesets.Catch.UI this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); Trail = true; - var hyperDashEndGlow = createAdditiveSprite(true); + var hyperDashEndGlow = createAdditiveSprite(); - hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); - hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); hyperDashEndGlow.FadeOut(1200); hyperDashEndGlow.Expire(true); } @@ -358,39 +361,36 @@ namespace osu.Game.Rulesets.Catch.UI private void updateCatcher() { - catcherIdle.Hide(); - catcherKiai.Hide(); - catcherFail.Hide(); - - CatcherSprite current; + currentCatcher?.Hide(); switch (CurrentState) { default: - current = catcherIdle; + currentCatcher = catcherIdle; break; case CatcherAnimationState.Fail: - current = catcherFail; + currentCatcher = catcherFail; break; case CatcherAnimationState.Kiai: - current = catcherKiai; + currentCatcher = catcherKiai; break; } - current.Show(); - (current.Drawable as IAnimation)?.GotoFrame(0); + currentCatcher.Show(); + (currentCatcher.Drawable as IAnimation)?.GotoFrame(0); } private void beginTrail() { - Trail &= dashing || HyperDashing; - Trail &= AdditiveTarget != null; + if (!dashing && !HyperDashing) + { + Trail = false; + return; + } - if (!Trail) return; - - var additive = createAdditiveSprite(HyperDashing); + var additive = createAdditiveSprite(); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); additive.Expire(true); @@ -428,6 +428,25 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } + private CatcherTrailSprite createAdditiveSprite() + { + var tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; + + var sprite = new CatcherTrailSprite(tex) + { + Anchor = Anchor, + Scale = Scale, + Colour = HyperDashing ? Color4.Red : Color4.White, + Blending = BlendingParameters.Additive, + RelativePositionAxes = RelativePositionAxes, + Position = Position + }; + + AdditiveTarget?.Add(sprite); + + return sprite; + } + private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) { if (ExplodingFruitTarget != null) From 74c9d5fc93f85780df7da15124e92b386e4ea517 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Mar 2020 13:45:55 +0300 Subject: [PATCH 269/387] Use AccentColour --- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/Pieces/SliderBall.cs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7403649184..ccc731779d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplySkin(skin, allowFallback); bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; - Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White; + Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 287a2d7e92..0c046604c0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -16,17 +16,24 @@ using osu.Game.Rulesets.Osu.Skinning; using osuTK.Graphics; using osu.Game.Skinning; using osuTK; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition + public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour { public Func GetInitialHitAction; - public new Color4 Colour + private Color4 accentColour; + + public Color4 AccentColour { - get => ball.Colour; - set => ball.Colour = value; + get => accentColour; + set + { + accentColour = value; + ball.Colour = value; + } } private readonly Slider slider; From c271d1755715822bd0dd5d47040a91da74ab23f6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Mar 2020 14:07:52 +0300 Subject: [PATCH 270/387] Remove useless field --- .../Objects/Drawables/Pieces/SliderBall.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 0c046604c0..5a6dd49c44 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -24,16 +24,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public Func GetInitialHitAction; - private Color4 accentColour; - public Color4 AccentColour { - get => accentColour; - set - { - accentColour = value; - ball.Colour = value; - } + get => ball.Colour; + set => ball.Colour = value; } private readonly Slider slider; From 62ce5031269446adbc8c107fa16f832678a5d441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 00:36:21 +0900 Subject: [PATCH 271/387] Fix changelog alignment and italics usage --- osu.Game/Overlays/Changelog/ChangelogBuild.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 8aee76cb08..48bf6c2ddd 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -93,6 +93,7 @@ namespace osu.Game.Overlays.Changelog Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.BottomLeft, } } }; @@ -125,7 +126,7 @@ namespace osu.Game.Overlays.Changelog title.AddText("by ", t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; t.Padding = new MarginPadding { Left = 10 }; }); @@ -138,7 +139,7 @@ namespace osu.Game.Overlays.Changelog Id = entry.GithubUser.UserId.Value }, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } @@ -146,7 +147,7 @@ namespace osu.Game.Overlays.Changelog { title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } @@ -154,7 +155,7 @@ namespace osu.Game.Overlays.Changelog { title.AddText(entry.GithubUser.DisplayName, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } From cd604785a87e14dfec1bf2317762f44325d1c196 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 00:38:27 +0900 Subject: [PATCH 272/387] Ignore italics specification for now --- osu.Game/Graphics/OsuFont.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 076cc241ed..7c78141b4d 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -31,7 +31,14 @@ namespace osu.Game.Graphics /// Whether all characters should be spaced the same distance apart. /// The . public static FontUsage GetFont(Typeface typeface = Typeface.Torus, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) - => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), italics, fixedWidth); + => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), getItalics(italics), fixedWidth); + + private static bool getItalics(in bool italicsRequested) + { + // right now none of our fonts support italics. + // should add exceptions to this rule if they come up. + return false; + } /// /// Retrieves the string representation of a . From 0b788065b46a0a579e849bfe893ba0b3b57d301a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 01:00:25 +0900 Subject: [PATCH 273/387] Update resources package --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f623a92ade..1a63b893a1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba6f0e2251..95d09e84eb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 54cd400d51..ce7ff38988 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + From 1a6056637b9918f66ad0dc5282d660665c17ebdc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 01:43:29 +0900 Subject: [PATCH 274/387] Turn off italics test for now (may come back if we switch chat to content font) --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index c76d4fd5b8..7a257a1603 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Chat; @@ -78,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount); AddAssert($"msg #{index} has the right action", hasExpectedActions); - AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); + //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks); bool hasExpectedActions() @@ -97,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online return true; } - bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); + //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); bool isShowingLinks() { From 12b7727af6022d1d4f88397f542a12497899b7be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 03:25:01 +0900 Subject: [PATCH 275/387] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1a63b893a1..66a1523843 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 95d09e84eb..647f05b428 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ce7ff38988..0e5c64cf0f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 340d362d69230a79a005cf86cc4bee0295709e00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 03:51:30 +0900 Subject: [PATCH 276/387] Appease inspectcode --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 20f9bb1be8..e2e7ba8031 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -779,7 +779,7 @@ namespace osu.Game.Screens.Select /// public bool UserScrolling { get; private set; } - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { UserScrolling = true; base.OnUserScroll(value, animated, distanceDecay); From f90485994367f5b6b1b57da6cb452536f92cc04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Mar 2020 15:45:13 +0100 Subject: [PATCH 277/387] Remove leftover unused private methods --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 29bed00d61..e361b29a9d 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -398,27 +398,6 @@ namespace osu.Game.Rulesets.Catch.UI Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } - private Drawable createAdditiveSprite(bool hyperDash) - { - var additive = createCatcherSprite(); - - additive.Anchor = Anchor; - additive.Scale = Scale; - additive.Colour = hyperDash ? Color4.Red : Color4.White; - additive.Blending = BlendingParameters.Additive; - additive.RelativePositionAxes = RelativePositionAxes; - additive.Position = Position; - - AdditiveTarget.Add(additive); - - return additive; - } - - private Drawable createCatcherSprite() - { - return new CatcherSprite(CurrentState); - } - private void updateState(CatcherAnimationState state) { if (CurrentState == state) From e68d4f92f5f5694d306a8b11d35eb7545685de4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Mar 2020 01:19:10 +0900 Subject: [PATCH 278/387] Fix framework version mismatch --- osu.iOS.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.iOS.props b/osu.iOS.props index 1387026799..0e5c64cf0f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -79,7 +79,7 @@ - + From acd280c85552ec22073606807e75eeda663ebe11 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Sun, 15 Mar 2020 22:13:26 +0100 Subject: [PATCH 279/387] Add System channel type and use it for the ChannelSelectorTab --- osu.Game/Online/Chat/ChannelType.cs | 1 + osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs index 7d2b661164..151efc4645 100644 --- a/osu.Game/Online/Chat/ChannelType.cs +++ b/osu.Game/Online/Chat/ChannelType.cs @@ -12,5 +12,6 @@ namespace osu.Game.Online.Chat Temporary, PM, Group, + System, } } diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs index 5fb56a3f75..e3ede04edd 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Chat.Tabs public ChannelSelectorTabChannel() { Name = "+"; - Type = ChannelType.Temporary; + Type = ChannelType.System; } } } From f390c1995db413005cea88a68d832e92382768e2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 16 Mar 2020 11:29:28 +0900 Subject: [PATCH 280/387] Apply comment suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Dean Herbert Co-Authored-By: Bartłomiej Dach --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 4 ++-- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 61bd962648..526bc668af 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -61,7 +61,7 @@ namespace osu.Game.Beatmaps /// /// The to create a playable for. /// The s to apply to the . - /// The loading timeout. + /// The maximum length in milliseconds to wait for load to complete. Defaults to 10,000ms. /// The converted . /// If could not be converted to . IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index cf13f8a3a1..f1cbed57f1 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -93,8 +93,8 @@ namespace osu.Game.Screens.Edit } catch (Exception e) { - Logger.Error(e, "Could not load beatmap sucessfully!"); - //couldn't load, hard abort! + Logger.Error(e, "Could not load beatmap successfully!"); + // couldn't load, hard abort! this.Exit(); return; } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 59d2aca17d..7a8a1593b9 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -331,7 +331,7 @@ namespace osu.Game.Screens.Select } catch (Exception e) { - Logger.Error(e, "Could not load beatmap sucessfully!"); + Logger.Error(e, "Could not load beatmap successfully!"); } } From 9c5423734a30587063c9a1375eeccb6243cc0d78 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Mar 2020 11:33:26 +0900 Subject: [PATCH 281/387] Throw timeout exceptions instead --- osu.Game/Beatmaps/WorkingBeatmap.cs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index bdcfc058b4..dd4f893ac2 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -100,7 +100,9 @@ namespace osu.Game.Beatmaps // Apply conversion mods foreach (var mod in mods.OfType()) { - cancellationSource.Token.ThrowIfCancellationRequested(); + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + mod.ApplyToBeatmapConverter(converter); } @@ -115,7 +117,9 @@ namespace osu.Game.Beatmaps foreach (var mod in mods.OfType()) { - cancellationSource.Token.ThrowIfCancellationRequested(); + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); } } @@ -127,7 +131,9 @@ namespace osu.Game.Beatmaps // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed foreach (var obj in converted.HitObjects) { - cancellationSource.Token.ThrowIfCancellationRequested(); + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); } @@ -135,7 +141,9 @@ namespace osu.Game.Beatmaps { foreach (var obj in converted.HitObjects) { - cancellationSource.Token.ThrowIfCancellationRequested(); + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + mod.ApplyToHitObject(obj); } } @@ -315,5 +323,13 @@ namespace osu.Game.Beatmaps private void recreate() => lazy = new Lazy(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); } + + private class BeatmapLoadTimeoutException : TimeoutException + { + public BeatmapLoadTimeoutException(BeatmapInfo beatmapInfo) + : base($"Timed out while loading beatmap ({beatmapInfo}).") + { + } + } } } From 58fc947be3417ce9edbeadbd00b885bcb569dba7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Mar 2020 14:06:42 +0900 Subject: [PATCH 282/387] Privatise some setters --- osu.Game/Users/UserPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 5676113aad..289244cdc3 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -32,9 +32,9 @@ namespace osu.Game.Users public new Action Action; - protected Action ViewProfile; + protected Action ViewProfile { get; private set; } - protected DelayedLoadUnloadWrapper Background; + protected DelayedLoadUnloadWrapper Background { get; private set; } private SpriteIcon statusIcon; private OsuSpriteText statusMessage; From 544dfe7dd39266a4e0c4906413fa3b26db161d10 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 16 Mar 2020 09:42:21 +0300 Subject: [PATCH 283/387] Implement FriendsLayout component --- .../Visual/Online/TestSceneFriendsLayout.cs | 80 ++++++ .../TestSceneFriendsOnlineStatusControl.cs | 12 +- .../UserInterface/TestSceneUserListToolbar.cs | 2 +- .../Online/API/Requests/GetFriendsRequest.cs | 4 +- .../API/Requests/Responses/APIFriend.cs | 14 + .../Friends/FriendsBundle.cs | 13 +- .../Dashboard/Friends/FriendsLayout.cs | 256 ++++++++++++++++++ .../Friends/FriendsOnlineStatusControl.cs | 15 +- .../Friends/FriendsOnlineStatusItem.cs | 2 +- .../Friends/UserListToolbar.cs | 2 +- .../Friends/UserSortTabControl.cs | 2 +- osu.Game/Overlays/SocialOverlay.cs | 2 +- 12 files changed, 379 insertions(+), 25 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APIFriend.cs rename osu.Game/Overlays/{Home => Dashboard}/Friends/FriendsBundle.cs (55%) create mode 100644 osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs rename osu.Game/Overlays/{Home => Dashboard}/Friends/FriendsOnlineStatusControl.cs (70%) rename osu.Game/Overlays/{Home => Dashboard}/Friends/FriendsOnlineStatusItem.cs (96%) rename osu.Game/Overlays/{Home => Dashboard}/Friends/UserListToolbar.cs (96%) rename osu.Game/Overlays/{Home => Dashboard}/Friends/UserSortTabControl.cs (90%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs new file mode 100644 index 0000000000..0e9fafb1b6 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -0,0 +1,80 @@ +// 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 osu.Framework.Graphics.Containers; +using osu.Game.Overlays.Dashboard.Friends; +using osu.Framework.Graphics; +using osu.Game.Users; +using osu.Game.Overlays; +using osu.Framework.Allocation; +using NUnit.Framework; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneFriendsLayout : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(FriendsLayout), + typeof(FriendsOnlineStatusControl), + typeof(UserListToolbar) + }; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + private FriendsLayout layout; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = layout = new FriendsLayout() + }; + }); + + [Test] + public void TestPopulate() + { + AddStep("Populate", () => layout.Users = getUsers()); + } + + private List getUsers() => new List + { + new APIFriend + { + Username = @"flyte", + Id = 3103765, + IsOnline = true, + CurrentModeRank = 1111, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }, + new APIFriend + { + Username = @"peppy", + Id = 2, + IsOnline = false, + CurrentModeRank = 2222, + Country = new Country { FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + IsSupporter = true, + SupportLevel = 3, + }, + new APIFriend + { + Username = @"Evast", + Id = 8195163, + Country = new Country { FlagName = @"BY" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsOnline = false, + LastVisit = DateTimeOffset.Now + } + }; + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 0d841dfef1..8bdf3c5dc1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -7,9 +7,9 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; -using osu.Game.Overlays.Home.Friends; -using osu.Game.Users; +using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Tests.Visual.UserInterface { @@ -39,17 +39,17 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void Populate() { - AddStep("Populate", () => control.Populate(new List + AddStep("Populate", () => control.Populate(new List { - new User + new APIFriend { IsOnline = true }, - new User + new APIFriend { IsOnline = false }, - new User + new APIFriend { IsOnline = false } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs index 02b8839922..1546972580 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osu.Game.Overlays.Home.Friends; +using osu.Game.Overlays.Dashboard.Friends; using osuTK; namespace osu.Game.Tests.Visual.UserInterface diff --git a/osu.Game/Online/API/Requests/GetFriendsRequest.cs b/osu.Game/Online/API/Requests/GetFriendsRequest.cs index 46890aa889..321f675aae 100644 --- a/osu.Game/Online/API/Requests/GetFriendsRequest.cs +++ b/osu.Game/Online/API/Requests/GetFriendsRequest.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Game.Users; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetFriendsRequest : APIRequest> + public class GetFriendsRequest : APIRequest> { protected override string Target => @"friends"; } diff --git a/osu.Game/Online/API/Requests/Responses/APIFriend.cs b/osu.Game/Online/API/Requests/Responses/APIFriend.cs new file mode 100644 index 0000000000..91fed28d44 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIFriend.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; +using osu.Game.Users; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIFriend : User + { + [JsonProperty(@"current_mode_rank")] + public int? CurrentModeRank; + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs similarity index 55% rename from osu.Game/Overlays/Home/Friends/FriendsBundle.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs index 75d00dfef8..0062c49c91 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs @@ -1,18 +1,23 @@ // 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.Overlays.Home.Friends +using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsBundle { public FriendsOnlineStatus Status { get; } - public int Count { get; } + public int Count => Users.Count; - public FriendsBundle(FriendsOnlineStatus status, int count) + public List Users { get; } + + public FriendsBundle(FriendsOnlineStatus status, List users) { Status = status; - Count = count; + Users = users; } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs new file mode 100644 index 0000000000..55f394cb78 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -0,0 +1,256 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Dashboard.Friends +{ + public class FriendsLayout : CompositeDrawable + { + private List users = new List(); + + public List Users + { + get => users; + set + { + users = value; + + usersLoaded = true; + + onlineStatusControl.Populate(value); + } + } + + [Resolved] + private IAPIProvider api { get; set; } + + private GetFriendsRequest request; + private CancellationTokenSource cancellationToken; + + private Drawable currentContent; + + private readonly Box background; + private readonly Box controlBackground; + private readonly FriendsOnlineStatusControl onlineStatusControl; + private readonly UserListToolbar userListToolbar; + private readonly Container itemsPlaceholder; + private readonly LoadingLayer loading; + + public FriendsLayout() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + controlBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Top = 20, + Horizontal = 45 + }, + Child = onlineStatusControl = new FriendsOnlineStatusControl(), + } + } + }, + new Container + { + Name = "User List", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Bottom = 20 }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Horizontal = 40, + Vertical = 20 + }, + Child = userListToolbar = new UserListToolbar + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + itemsPlaceholder = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 50 } + }, + loading = new LoadingLayer(itemsPlaceholder) + } + } + } + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + background.Colour = colourProvider.Background4; + controlBackground.Colour = colourProvider.Background5; + } + + private bool usersLoaded; + + protected override void LoadComplete() + { + base.LoadComplete(); + + onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); + userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); + userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); + + if (!api.IsLoggedIn) + return; + + request = new GetFriendsRequest(); + request.Success += response => Schedule(() => Users = response); + api.Queue(request); + } + + private void recreatePanels() + { + // Don't allow any changes until we have users loaded + if (!usersLoaded) + return; + + cancellationToken?.Cancel(); + + if (itemsPlaceholder.Any()) + loading.Show(); + + var groupedUsers = onlineStatusControl.Current.Value?.Users ?? new List(); + + var sortedUsers = sortUsers(groupedUsers); + + LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + } + + private void addContentToPlaceholder(Drawable content) + { + loading.Hide(); + + var lastContent = currentContent; + + if (lastContent != null) + { + lastContent.FadeOut(100, Easing.OutQuint).Expire(); + lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y); + } + + itemsPlaceholder.Add(currentContent = content); + currentContent.FadeIn(200, Easing.OutQuint); + } + + private FillFlowContainer createTable(List users) + { + var style = userListToolbar.DisplayStyle.Value; + + return new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), + Children = users.Select(u => createUserPanel(u, style)).ToList() + }; + } + + private UserPanel createUserPanel(User user, OverlayPanelDisplayStyle style) + { + switch (style) + { + default: + case OverlayPanelDisplayStyle.Card: + return new UserGridPanel(user).With(panel => + { + panel.Anchor = Anchor.TopCentre; + panel.Origin = Anchor.TopCentre; + panel.Width = 290; + }); + + case OverlayPanelDisplayStyle.List: + return new UserListPanel(user); + } + } + + private List sortUsers(List unsorted) + { + switch (userListToolbar.SortCriteria.Value) + { + default: + case UserSortCriteria.LastVisit: + return unsorted.OrderBy(u => u.LastVisit).Reverse().ToList(); + + case UserSortCriteria.Rank: + return unsorted.Where(u => u.CurrentModeRank.HasValue).OrderBy(u => u.CurrentModeRank).Concat(unsorted.Where(u => u.CurrentModeRank == null)).ToList(); + + case UserSortCriteria.Username: + return unsorted.OrderBy(u => u.Username).ToList(); + } + } + + protected override void Dispose(bool isDisposing) + { + request?.Cancel(); + cancellationToken?.Cancel(); + + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs similarity index 70% rename from osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs index 196f01ab4a..2b716f228d 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs @@ -3,22 +3,21 @@ using System.Collections.Generic; using System.Linq; -using osu.Game.Users; +using osu.Game.Online.API.Requests.Responses; -namespace osu.Game.Overlays.Home.Friends +namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsOnlineStatusControl : OverlayStreamControl { protected override OverlayStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); - public void Populate(List users) + public void Populate(List users) { - var userCount = users.Count; - var onlineUsersCount = users.Count(user => user.IsOnline); + Clear(); - AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.All, users)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Online, users.Where(u => u.IsOnline).ToList())); + AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, users.Where(u => !u.IsOnline).ToList())); Current.Value = Items.FirstOrDefault(); } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs similarity index 96% rename from osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index d9b780ce46..eada9420ea 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs @@ -5,7 +5,7 @@ using System; using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Overlays.Home.Friends +namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsOnlineStatusItem : OverlayStreamItem { diff --git a/osu.Game/Overlays/Home/Friends/UserListToolbar.cs b/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs similarity index 96% rename from osu.Game/Overlays/Home/Friends/UserListToolbar.cs rename to osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs index f7c5e9f4fd..fb4b938183 100644 --- a/osu.Game/Overlays/Home/Friends/UserListToolbar.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Containers; using osuTK; using osu.Framework.Bindables; -namespace osu.Game.Overlays.Home.Friends +namespace osu.Game.Overlays.Dashboard.Friends { public class UserListToolbar : CompositeDrawable { diff --git a/osu.Game/Overlays/Home/Friends/UserSortTabControl.cs b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs similarity index 90% rename from osu.Game/Overlays/Home/Friends/UserSortTabControl.cs rename to osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs index 2479fa4638..3a5f65212d 100644 --- a/osu.Game/Overlays/Home/Friends/UserSortTabControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs @@ -3,7 +3,7 @@ using System.ComponentModel; -namespace osu.Game.Overlays.Home.Friends +namespace osu.Game.Overlays.Dashboard.Friends { public class UserSortTabControl : OverlaySortTabControl { diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 02f7c9b0d3..ba572b0e78 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -120,7 +120,7 @@ namespace osu.Game.Overlays { case SocialTab.Friends: var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += users => Users = users.ToArray(); + friendRequest.Success += users => Users = users.Select(u => (User)u).ToArray(); API.Queue(getUsersRequest = friendRequest); break; From 24fe7538fd0e7eb7d9592760978e716636751069 Mon Sep 17 00:00:00 2001 From: "Marcus \"Mestro\" Nordgren" Date: Mon, 16 Mar 2020 13:09:15 +0100 Subject: [PATCH 284/387] Use new logo name for showcase screen --- osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs index 6ad5ccaf0c..bd5aa2f5d9 100644 --- a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs +++ b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Screens.Showcase Origin = Anchor.TopCentre, FillMode = FillMode.Fit, RelativeSizeAxes = Axes.Both, - Texture = textures.Get("game-screen-logo"), + Texture = textures.Get("header-logo"), }; } } From 50c2e65e3c2b5da4378171d3c920e1648521e4f0 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Mon, 16 Mar 2020 19:10:42 +0100 Subject: [PATCH 285/387] Improve TestSceneChatOverlay --- .../Visual/Online/TestSceneChatOverlay.cs | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 736bfd8e7d..03251f1d5e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -47,14 +47,20 @@ namespace osu.Game.Tests.Visual.Online private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); private Channel channel1 => channels[0]; private Channel channel2 => channels[1]; + private Channel channelPM => channels.Last(); public TestSceneChatOverlay() { channels = Enumerable.Range(1, 10) - .Select(index => new Channel(new User()) + .Select(index => new Channel { Name = $"Channel no. {index}", - Topic = index == 3 ? null : $"We talk about the number {index} here" + Topic = index == 3 ? null : $"We talk about the number {index} here", + Type = ChannelType.Temporary + }) + .Append(new Channel(new User()) + { + Name = "PM channel" }) .ToList(); } @@ -100,28 +106,11 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); } - [Test] - public void TestCloseChannelWhileSelectorClosed() - { - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); - - AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); - - AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); - AddAssert("Current channel is channel 1", () => currentChannel == channel1); - - AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); - - AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); - } - [Test] public void TestSearchInSelector() { - AddStep("search for 'no. 2'", () => chatOverlay.ChildrenOfType().First().Text = "no. 2"); - AddUntilStep("only channel 2 visible", () => + AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType().First().Text = "no. 2"); + AddUntilStep("Only channel 2 visible", () => { var listItems = chatOverlay.ChildrenOfType().Where(c => c.IsPresent); return listItems.Count() == 1 && listItems.Single().Channel == channel2; @@ -131,28 +120,28 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChannelShortcutKeys() { - AddStep("join 10 channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel))); - AddStep("close channel selector", () => + AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel))); + AddStep("Close channel selector", () => { InputManager.PressKey(Key.Escape); InputManager.ReleaseKey(Key.Escape); }); - AddUntilStep("wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); + AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); for (int zeroBasedIndex = 0; zeroBasedIndex < 10; ++zeroBasedIndex) { var oneBasedIndex = zeroBasedIndex + 1; var targetNumberKey = oneBasedIndex % 10; var targetChannel = channels[zeroBasedIndex]; - AddStep($"press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); - AddAssert($"channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel); + AddStep($"Press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); + AddAssert($"Channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel); } } private Channel expectedChannel; [Test] - public void TestCloseChannelWhileActive() + public void TestCloseChannelBehaviour() { AddUntilStep("Join until dropdown has channels", () => { @@ -160,8 +149,11 @@ namespace osu.Game.Tests.Visual.Online return true; // Using temporary channels because they don't hide their names when not active - Channel toAdd = new Channel { Name = $"test channel {joinedChannels.Count()}", Type = ChannelType.Temporary }; - channelManager.JoinChannel(toAdd); + channelManager.JoinChannel(new Channel + { + Name = $"Channel no. {joinedChannels.Count() + 1}", + Type = ChannelType.Temporary + }); return false; }); @@ -176,6 +168,7 @@ namespace osu.Game.Tests.Visual.Online chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); }); AddAssert("Next channel selected", () => currentChannel == expectedChannel); + AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); // Depending on the window size, one more channel might need to be closed for the selectorTab to appear AddUntilStep("Close channels until selector visible", () => @@ -194,7 +187,7 @@ namespace osu.Game.Tests.Visual.Online expectedChannel = previousChannel; chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); }); - AddAssert("Channel changed to previous", () => currentChannel == expectedChannel); + AddAssert("Previous channel selected", () => currentChannel == expectedChannel); // Standard channel closing AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1)); @@ -203,7 +196,38 @@ namespace osu.Game.Tests.Visual.Online expectedChannel = nextChannel; chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); }); - AddAssert("Channel changed to next", () => currentChannel == expectedChannel); + AddAssert("Next channel selected", () => currentChannel == expectedChannel); + + // Selector reappearing after all channels closed + AddUntilStep("Close all channels", () => + { + if (!joinedChannels.Any()) + return true; + + chatOverlay.ChannelTabControl.RemoveChannel(joinedChannels.Last()); + return false; + }); + AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); + } + + [Test] + public void TestChannelCloseButton() + { + AddStep("Join channels", () => + { + channelManager.JoinChannel(channel1); + channelManager.JoinChannel(channelPM); + }); + + // PM channel close button only appears when active + AddStep("Select PM channel", () => clickDrawable(chatOverlay.TabMap[channelPM])); + AddStep("Click PM close button", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channelPM]).CloseButton.Child)); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(channelPM)); + + // Non-PM chat channel close button only appears when hovered + AddStep("Hover normal channel tab", () => InputManager.MoveMouseTo(chatOverlay.TabMap[channel1])); + AddStep("Click normal close button", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any()); } private void pressChannelHotkey(int number) From 0f40671e69ea9d2baaa426f0d841223c2bb65959 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Mon, 16 Mar 2020 19:44:03 +0100 Subject: [PATCH 286/387] Mix normal channel tabs with PM ones --- .../Visual/Online/TestSceneChatOverlay.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 03251f1d5e..02460282d8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -47,20 +47,15 @@ namespace osu.Game.Tests.Visual.Online private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); private Channel channel1 => channels[0]; private Channel channel2 => channels[1]; - private Channel channelPM => channels.Last(); public TestSceneChatOverlay() { channels = Enumerable.Range(1, 10) - .Select(index => new Channel + .Select(index => new Channel(new User()) { Name = $"Channel no. {index}", Topic = index == 3 ? null : $"We talk about the number {index} here", - Type = ChannelType.Temporary - }) - .Append(new Channel(new User()) - { - Name = "PM channel" + Type = index % 2 == 0 ? ChannelType.PM : ChannelType.Temporary }) .ToList(); } @@ -150,9 +145,9 @@ namespace osu.Game.Tests.Visual.Online // Using temporary channels because they don't hide their names when not active channelManager.JoinChannel(new Channel - { + { Name = $"Channel no. {joinedChannels.Count() + 1}", - Type = ChannelType.Temporary + Type = ChannelType.Temporary }); return false; @@ -203,7 +198,7 @@ namespace osu.Game.Tests.Visual.Online { if (!joinedChannels.Any()) return true; - + chatOverlay.ChannelTabControl.RemoveChannel(joinedChannels.Last()); return false; }); @@ -213,16 +208,16 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChannelCloseButton() { - AddStep("Join channels", () => + AddStep("Join 2 channels", () => { channelManager.JoinChannel(channel1); - channelManager.JoinChannel(channelPM); + channelManager.JoinChannel(channel2); }); // PM channel close button only appears when active - AddStep("Select PM channel", () => clickDrawable(chatOverlay.TabMap[channelPM])); - AddStep("Click PM close button", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channelPM]).CloseButton.Child)); - AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(channelPM)); + AddStep("Select PM channel", () => clickDrawable(chatOverlay.TabMap[channel2])); + AddStep("Click PM close button", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(channel2)); // Non-PM chat channel close button only appears when hovered AddStep("Hover normal channel tab", () => InputManager.MoveMouseTo(chatOverlay.TabMap[channel1])); From 4153f8d49db453b613f7bd1ed7624892d060f3b3 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Mon, 16 Mar 2020 21:31:22 +0100 Subject: [PATCH 287/387] Fix edge case making test fail Forgot that if a PM channel was the last tab, it hid itself upon selecting due to changing its width, which made the last-visible-selected assert fail. Made this particular test only use non-PM channels. --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 02460282d8..6665452d94 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.Online // Using temporary channels because they don't hide their names when not active channelManager.JoinChannel(new Channel { - Name = $"Channel no. {joinedChannels.Count() + 1}", + Name = $"Channel no. {joinedChannels.Count() + 11}", Type = ChannelType.Temporary }); From cc5833db80568e8e300dd434f4ad636c9582f573 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:36:48 +0300 Subject: [PATCH 288/387] Remove string prefixes in the test scene --- .../Visual/Online/TestSceneFriendsLayout.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 0e9fafb1b6..46f22073f2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -48,30 +48,30 @@ namespace osu.Game.Tests.Visual.Online { new APIFriend { - Username = @"flyte", + Username = "flyte", Id = 3103765, IsOnline = true, CurrentModeRank = 1111, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + Country = new Country { FlagName = "JP" }, + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }, new APIFriend { - Username = @"peppy", + Username = "peppy", Id = 2, IsOnline = false, CurrentModeRank = 2222, - Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + Country = new Country { FlagName = "AU" }, + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", IsSupporter = true, SupportLevel = 3, }, new APIFriend { - Username = @"Evast", + Username = "Evast", Id = 8195163, - Country = new Country { FlagName = @"BY" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + Country = new Country { FlagName = "BY" }, + CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", IsOnline = false, LastVisit = DateTimeOffset.Now } From 6ec01a67af0d3d3a7af3db542e00dd69e725b008 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:38:45 +0300 Subject: [PATCH 289/387] Use cast in SocialOverlay --- osu.Game/Overlays/SocialOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index ba572b0e78..ff6f7de436 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -120,7 +120,7 @@ namespace osu.Game.Overlays { case SocialTab.Friends: var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += users => Users = users.Select(u => (User)u).ToArray(); + friendRequest.Success += users => Users = users.Cast().ToArray(); API.Queue(getUsersRequest = friendRequest); break; From 6a151b8e75a4bcc3ed48d2dfd6c8f2f2ddd393b1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:50:19 +0300 Subject: [PATCH 290/387] Add online test --- .../Visual/Online/TestSceneFriendsLayout.cs | 18 ++++++++++++++++-- .../Dashboard/Friends/FriendsLayout.cs | 12 +++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 46f22073f2..90474e5178 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -11,6 +11,7 @@ using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; using osu.Game.Online.API.Requests.Responses; +using System.Linq; namespace osu.Game.Tests.Visual.Online { @@ -23,10 +24,12 @@ namespace osu.Game.Tests.Visual.Online typeof(UserListToolbar) }; + protected override bool UseOnlineAPI => true; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private FriendsLayout layout; + private TestFriendsLayout layout; [SetUp] public void Setup() => Schedule(() => @@ -34,10 +37,16 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = layout = new FriendsLayout() + Child = layout = new TestFriendsLayout() }; }); + [Test] + public void TestOnline() + { + AddUntilStep("Users loaded", () => layout?.StatusControl.Items.Any() ?? false); + } + [Test] public void TestPopulate() { @@ -76,5 +85,10 @@ namespace osu.Game.Tests.Visual.Online LastVisit = DateTimeOffset.Now } }; + + private class TestFriendsLayout : FriendsLayout + { + public FriendsOnlineStatusControl StatusControl => OnlineStatusControl; + } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 55f394cb78..cd358bcf84 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -26,11 +26,13 @@ namespace osu.Game.Overlays.Dashboard.Friends get => users; set { + request?.Cancel(); + users = value; usersLoaded = true; - onlineStatusControl.Populate(value); + OnlineStatusControl.Populate(value); } } @@ -42,9 +44,9 @@ namespace osu.Game.Overlays.Dashboard.Friends private Drawable currentContent; + protected readonly FriendsOnlineStatusControl OnlineStatusControl; private readonly Box background; private readonly Box controlBackground; - private readonly FriendsOnlineStatusControl onlineStatusControl; private readonly UserListToolbar userListToolbar; private readonly Container itemsPlaceholder; private readonly LoadingLayer loading; @@ -78,7 +80,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Top = 20, Horizontal = 45 }, - Child = onlineStatusControl = new FriendsOnlineStatusControl(), + Child = OnlineStatusControl = new FriendsOnlineStatusControl(), } } }, @@ -152,7 +154,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { base.LoadComplete(); - onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); + OnlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); @@ -175,7 +177,7 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var groupedUsers = onlineStatusControl.Current.Value?.Users ?? new List(); + var groupedUsers = OnlineStatusControl.Current.Value?.Users ?? new List(); var sortedUsers = sortUsers(groupedUsers); From da97a02e6660ef516762d9f0c742df3eb39f7247 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:53:53 +0300 Subject: [PATCH 291/387] Remove pointless flag --- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index cd358bcf84..1098fdee8f 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -30,8 +30,6 @@ namespace osu.Game.Overlays.Dashboard.Friends users = value; - usersLoaded = true; - OnlineStatusControl.Populate(value); } } @@ -148,8 +146,6 @@ namespace osu.Game.Overlays.Dashboard.Friends controlBackground.Colour = colourProvider.Background5; } - private bool usersLoaded; - protected override void LoadComplete() { base.LoadComplete(); @@ -168,8 +164,7 @@ namespace osu.Game.Overlays.Dashboard.Friends private void recreatePanels() { - // Don't allow any changes until we have users loaded - if (!usersLoaded) + if (!users.Any()) return; cancellationToken?.Cancel(); From bd84980aa61af03d6c398db2942d8d5998d049ab Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:56:10 +0300 Subject: [PATCH 292/387] Simplify order by last visit --- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 1098fdee8f..1c56227521 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -232,7 +232,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { default: case UserSortCriteria.LastVisit: - return unsorted.OrderBy(u => u.LastVisit).Reverse().ToList(); + return unsorted.OrderByDescending(u => u.LastVisit).ToList(); case UserSortCriteria.Rank: return unsorted.Where(u => u.CurrentModeRank.HasValue).OrderBy(u => u.CurrentModeRank).Concat(unsorted.Where(u => u.CurrentModeRank == null)).ToList(); From f816479ff8972c4e2fb139b55746b36432a05f34 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 02:03:57 +0300 Subject: [PATCH 293/387] Simplify order by rank --- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 1c56227521..f069d3f384 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -235,7 +235,7 @@ namespace osu.Game.Overlays.Dashboard.Friends return unsorted.OrderByDescending(u => u.LastVisit).ToList(); case UserSortCriteria.Rank: - return unsorted.Where(u => u.CurrentModeRank.HasValue).OrderBy(u => u.CurrentModeRank).Concat(unsorted.Where(u => u.CurrentModeRank == null)).ToList(); + return unsorted.OrderByDescending(u => u.CurrentModeRank.HasValue).ThenBy(u => u.CurrentModeRank ?? 0).ToList(); case UserSortCriteria.Username: return unsorted.OrderBy(u => u.Username).ToList(); From d9d812a8fe09d7006c0e64bd20a79dd9f049efec Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 02:10:20 +0300 Subject: [PATCH 294/387] Fix status icon flash on first status change --- osu.Game/Users/UserPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 289244cdc3..d5e6d5f13e 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -99,6 +99,9 @@ namespace osu.Game.Users { base.LoadComplete(); Status.TriggerChange(); + + // Colour should be applied immediately on first load. + statusIcon.FinishTransforms(); } protected override bool OnHover(HoverEvent e) From bf9c6f8a3b0f36f1fb984ddc7894cb00d2ec0a8d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 02:19:03 +0300 Subject: [PATCH 295/387] Skip online test if user is not logged-in --- osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 90474e5178..788cbd82c9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using NUnit.Framework; using osu.Game.Online.API.Requests.Responses; using System.Linq; +using osu.Game.Online.API; namespace osu.Game.Tests.Visual.Online { @@ -29,6 +30,9 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [Resolved] + private IAPIProvider api { get; set; } + private TestFriendsLayout layout; [SetUp] @@ -44,7 +48,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnline() { - AddUntilStep("Users loaded", () => layout?.StatusControl.Items.Any() ?? false); + // Skip online test if user is not logged-in + AddUntilStep("Users loaded", () => !api.IsLoggedIn || (layout?.StatusControl.Items.Any() ?? false)); } [Test] From f7ea20a926d9cf512ec4ec0533777336de3744c4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 16 Mar 2020 16:32:25 -0700 Subject: [PATCH 296/387] Limit font weight to bold --- osu.Game/Graphics/OsuFont.cs | 5 ----- osu.Game/Overlays/News/NewsArticleCover.cs | 2 +- osu.Game/Overlays/Notifications/NotificationSection.cs | 4 ++-- osu.Game/Overlays/OSD/Toast.cs | 2 +- osu.Game/Overlays/OverlayStreamItem.cs | 2 +- osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs | 2 +- osu.Game/Overlays/Settings/SettingsSubsection.cs | 2 +- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 2 +- osu.Game/Screens/Play/Break/BreakInfo.cs | 2 +- 9 files changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 7c78141b4d..255f7f24f7 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -136,10 +136,5 @@ namespace osu.Game.Graphics /// Equivalent to weight 700. ///
Bold = 700, - - /// - /// Equivalent to weight 900. - /// - Black = 900 } } diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs index e381b629e4..cca0cfb4a0 100644 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.News { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Black, false, false), + Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Bold, false, false), Text = date.ToString("d MMM yyy").ToUpper(), Margin = new MarginPadding { diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 17a2d4cf9f..c2a958b65e 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -84,13 +84,13 @@ namespace osu.Game.Overlays.Notifications new OsuSpriteText { Text = titleText.ToUpperInvariant(), - Font = OsuFont.GetFont(weight: FontWeight.Black) + Font = OsuFont.GetFont(weight: FontWeight.Bold) }, countDrawable = new OsuSpriteText { Text = "3", Colour = colours.Yellow, - Font = OsuFont.GetFont(weight: FontWeight.Black) + Font = OsuFont.GetFont(weight: FontWeight.Bold) }, } }, diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 46c53ec409..5d36cac20e 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.OSD { Padding = new MarginPadding(10), Name = "Description", - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Black), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), Spacing = new Vector2(1, 0), Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs index 630d3a0a22..7f8559e7de 100644 --- a/osu.Game/Overlays/OverlayStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays new OsuSpriteText { Text = MainText, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), }, new OsuSpriteText { diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 3ab64786a2..52b712a40e 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { Text = "ACCOUNT", Margin = new MarginPadding { Bottom = 5 }, - Font = OsuFont.GetFont(weight: FontWeight.Black), + Font = OsuFont.GetFont(weight: FontWeight.Bold), }, form = new LoginForm { diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index 9b3b2f570c..b096c146a6 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings { Text = Header.ToUpperInvariant(), Margin = new MarginPadding { Bottom = 10, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, - Font = OsuFont.GetFont(weight: FontWeight.Black), + Font = OsuFont.GetFont(weight: FontWeight.Bold), }, FlowContent }); diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 96e3ab48f2..5c59cfbfe8 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Edit.Timing public HeaderText(string text) { Text = text.ToUpper(); - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black); + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold); } } diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs index a3d64d05a3..6e129b20ea 100644 --- a/osu.Game/Screens/Play/Break/BreakInfo.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play.Break Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = "current progress".ToUpperInvariant(), - Font = OsuFont.GetFont(weight: FontWeight.Black, size: 15), + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15), }, new FillFlowContainer { From 8895d52d298b8b474ec40dd5490d0543545a2782 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 13:16:18 +0900 Subject: [PATCH 297/387] Fix header-text scaling on intro/winner screens --- .../Components/TestSceneRoundDisplay.cs | 40 +++++++++++++++++++ .../DrawableTournamentHeaderText.cs | 11 ++--- .../Components/RoundDisplay.cs | 14 +++++-- 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs new file mode 100644 index 0000000000..6f71627ce4 --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs @@ -0,0 +1,40 @@ +// 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 osu.Framework.Graphics; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneRoundDisplay : TournamentTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableTournamentHeaderText), + typeof(DrawableTournamentHeaderLogo), + }; + + public TestSceneRoundDisplay() + { + Children = new Drawable[] + { + new RoundDisplay(new TournamentMatch + { + Round = + { + Value = new TournamentRound + { + Name = { Value = "Test Round" } + } + } + }) + { + Margin = new MarginPadding(20) + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs index bda696ba00..99d914fed4 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -11,9 +11,13 @@ namespace osu.Game.Tournament.Components { public class DrawableTournamentHeaderText : CompositeDrawable { - public DrawableTournamentHeaderText() + public DrawableTournamentHeaderText(bool center = true) { - InternalChild = new TextSprite(); + InternalChild = new TextSprite + { + Anchor = center ? Anchor.Centre : Anchor.TopLeft, + Origin = center ? Anchor.Centre : Anchor.TopLeft, + }; Height = 22; RelativeSizeAxes = Axes.X; @@ -27,9 +31,6 @@ namespace osu.Game.Tournament.Components RelativeSizeAxes = Axes.Both; FillMode = FillMode.Fit; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Texture = textures.Get("header-text"); } } diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs index bebede6782..c0002e6804 100644 --- a/osu.Game.Tournament/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Components/RoundDisplay.cs @@ -12,19 +12,27 @@ namespace osu.Game.Tournament.Components { public RoundDisplay(TournamentMatch match) { - AutoSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; InternalChildren = new Drawable[] { new FillFlowContainer { - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, Direction = FillDirection.Vertical, Children = new Drawable[] { - new DrawableTournamentHeaderText(), + new DrawableTournamentHeaderText(false) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, new TournamentSpriteText { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, Text = match.Round.Value?.Name.Value ?? "Unknown Round", Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold) }, From 99f28efc96a5b6b459a4b81ccccb8478e539fe02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 13:16:52 +0900 Subject: [PATCH 298/387] Automatically mark the currently selected match as stsrated on entering gameplay screen --- .../Gameplay/Components/TeamScoreDisplay.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index 462015f004..3e60a03f92 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -35,7 +35,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private void load(LadderInfo ladder) { currentMatch.BindTo(ladder.CurrentMatch); - currentMatch.BindValueChanged(matchChanged, true); + currentMatch.BindValueChanged(matchChanged); + + updateMatch(); } private void matchChanged(ValueChangedEvent match) @@ -43,10 +45,19 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentTeamScore.UnbindBindings(); currentTeam.UnbindBindings(); - if (match.NewValue != null) + Scheduler.AddOnce(updateMatch); + } + + private void updateMatch() + { + var match = currentMatch.Value; + + if (match != null) { - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + match.StartMatch(); + + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.Team1Score : match.Team2Score); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.Team1 : match.Team2); } // team may change to same team, which means score is not in a good state. From 4ac740b12baceb300e3c582e37804587568acb8c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 08:51:54 +0300 Subject: [PATCH 299/387] Remove APIFriend --- .../Visual/Online/TestSceneFriendsLayout.cs | 9 ++++----- .../TestSceneFriendsOnlineStatusControl.cs | 10 +++++----- osu.Game/Online/API/Requests/GetFriendsRequest.cs | 4 ++-- .../Online/API/Requests/Responses/APIFriend.cs | 14 -------------- .../Overlays/Dashboard/Friends/FriendsBundle.cs | 6 +++--- .../Overlays/Dashboard/Friends/FriendsLayout.cs | 11 +++++------ .../Friends/FriendsOnlineStatusControl.cs | 4 ++-- osu.Game/Overlays/SocialOverlay.cs | 2 +- osu.Game/Users/User.cs | 3 +++ 9 files changed, 25 insertions(+), 38 deletions(-) delete mode 100644 osu.Game/Online/API/Requests/Responses/APIFriend.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 788cbd82c9..c6971a971d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -10,7 +10,6 @@ using osu.Game.Users; using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; -using osu.Game.Online.API.Requests.Responses; using System.Linq; using osu.Game.Online.API; @@ -58,9 +57,9 @@ namespace osu.Game.Tests.Visual.Online AddStep("Populate", () => layout.Users = getUsers()); } - private List getUsers() => new List + private List getUsers() => new List { - new APIFriend + new User { Username = "flyte", Id = 3103765, @@ -69,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online Country = new Country { FlagName = "JP" }, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }, - new APIFriend + new User { Username = "peppy", Id = 2, @@ -80,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online IsSupporter = true, SupportLevel = 3, }, - new APIFriend + new User { Username = "Evast", Id = 8195163, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 8bdf3c5dc1..d72818ed89 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -7,9 +7,9 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; +using osu.Game.Users; namespace osu.Game.Tests.Visual.UserInterface { @@ -39,17 +39,17 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void Populate() { - AddStep("Populate", () => control.Populate(new List + AddStep("Populate", () => control.Populate(new List { - new APIFriend + new User { IsOnline = true }, - new APIFriend + new User { IsOnline = false }, - new APIFriend + new User { IsOnline = false } diff --git a/osu.Game/Online/API/Requests/GetFriendsRequest.cs b/osu.Game/Online/API/Requests/GetFriendsRequest.cs index 321f675aae..46890aa889 100644 --- a/osu.Game/Online/API/Requests/GetFriendsRequest.cs +++ b/osu.Game/Online/API/Requests/GetFriendsRequest.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; namespace osu.Game.Online.API.Requests { - public class GetFriendsRequest : APIRequest> + public class GetFriendsRequest : APIRequest> { protected override string Target => @"friends"; } diff --git a/osu.Game/Online/API/Requests/Responses/APIFriend.cs b/osu.Game/Online/API/Requests/Responses/APIFriend.cs deleted file mode 100644 index 91fed28d44..0000000000 --- a/osu.Game/Online/API/Requests/Responses/APIFriend.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Newtonsoft.Json; -using osu.Game.Users; - -namespace osu.Game.Online.API.Requests.Responses -{ - public class APIFriend : User - { - [JsonProperty(@"current_mode_rank")] - public int? CurrentModeRank; - } -} diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs index 0062c49c91..772d9c67a0 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { @@ -12,9 +12,9 @@ namespace osu.Game.Overlays.Dashboard.Friends public int Count => Users.Count; - public List Users { get; } + public List Users { get; } - public FriendsBundle(FriendsOnlineStatus status, List users) + public FriendsBundle(FriendsOnlineStatus status, List users) { Status = status; Users = users; diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index f069d3f384..c02f07fe4a 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Users; using osuTK; @@ -19,9 +18,9 @@ namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsLayout : CompositeDrawable { - private List users = new List(); + private List users = new List(); - public List Users + public List Users { get => users; set @@ -172,7 +171,7 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var groupedUsers = OnlineStatusControl.Current.Value?.Users ?? new List(); + var groupedUsers = OnlineStatusControl.Current.Value?.Users ?? new List(); var sortedUsers = sortUsers(groupedUsers); @@ -195,7 +194,7 @@ namespace osu.Game.Overlays.Dashboard.Friends currentContent.FadeIn(200, Easing.OutQuint); } - private FillFlowContainer createTable(List users) + private FillFlowContainer createTable(List users) { var style = userListToolbar.DisplayStyle.Value; @@ -226,7 +225,7 @@ namespace osu.Game.Overlays.Dashboard.Friends } } - private List sortUsers(List unsorted) + private List sortUsers(List unsorted) { switch (userListToolbar.SortCriteria.Value) { diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs index 2b716f228d..88035e0a34 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { protected override OverlayStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); - public void Populate(List users) + public void Populate(List users) { Clear(); diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index ff6f7de436..02f7c9b0d3 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -120,7 +120,7 @@ namespace osu.Game.Overlays { case SocialTab.Friends: var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += users => Users = users.Cast().ToArray(); + friendRequest.Success += users => Users = users.ToArray(); API.Queue(getUsersRequest = friendRequest); break; diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index d25c552160..2a6f7844a2 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -69,6 +69,9 @@ namespace osu.Game.Users [JsonProperty(@"support_level")] public int SupportLevel; + [JsonProperty(@"current_mode_rank")] + public int? CurrentModeRank; + [JsonProperty(@"is_gmt")] public bool IsGMT; From 9e7c388202b2cdc59b4b43a0bd55e54c83ed9135 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:24:33 +0900 Subject: [PATCH 300/387] Expose Spacing and UseFullGlyphHeight --- osu.Game/Graphics/Sprites/GlowingSpriteText.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 12688da9df..4aea5aa518 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -43,6 +43,18 @@ namespace osu.Game.Graphics.Sprites set => blurredText.Colour = value; } + public Vector2 Spacing + { + get => spriteText.Spacing; + set => spriteText.Spacing = blurredText.Spacing = value; + } + + public bool UseFullGlyphHeight + { + get => spriteText.UseFullGlyphHeight; + set => spriteText.UseFullGlyphHeight = blurredText.UseFullGlyphHeight = value; + } + public GlowingSpriteText() { AutoSizeAxes = Axes.Both; From d77b0acd906a3fbb1af6bf10ca1f82fff6b9c439 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:25:41 +0900 Subject: [PATCH 301/387] Move rank colour to OsuColour --- osu.Game/Graphics/OsuColour.cs | 30 +++++++++++++++++++ osu.Game/Online/Leaderboards/DrawableRank.cs | 31 +------------------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 984f5e52d1..f7ed55410c 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -3,6 +3,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Game.Beatmaps; +using osu.Game.Scoring; using osuTK.Graphics; namespace osu.Game.Graphics @@ -37,6 +38,35 @@ namespace osu.Game.Graphics } } + /// + /// Retrieves the colour for a . + /// + public static Color4 ForRank(ScoreRank rank) + { + switch (rank) + { + case ScoreRank.XH: + case ScoreRank.X: + return Color4Extensions.FromHex(@"ce1c9d"); + + case ScoreRank.SH: + case ScoreRank.S: + return Color4Extensions.FromHex(@"00a8b5"); + + case ScoreRank.A: + return Color4Extensions.FromHex(@"7cce14"); + + case ScoreRank.B: + return Color4Extensions.FromHex(@"e3b130"); + + case ScoreRank.C: + return Color4Extensions.FromHex(@"f18252"); + + default: + return Color4Extensions.FromHex(@"e95353"); + } + } + // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 45b91bbf81..0c3ab25044 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.Leaderboards FillMode = FillMode.Fit; FillAspectRatio = 2; - var rankColour = getRankColour(); + var rankColour = OsuColour.ForRank(rank); InternalChild = new DrawSizePreservingFillContainer { TargetDrawSize = new Vector2(64, 32), @@ -71,35 +71,6 @@ namespace osu.Game.Online.Leaderboards private string getRankName() => rank.GetDescription().TrimEnd('+'); - /// - /// Retrieves the grade background colour. - /// - private Color4 getRankColour() - { - switch (rank) - { - case ScoreRank.XH: - case ScoreRank.X: - return Color4Extensions.FromHex(@"ce1c9d"); - - case ScoreRank.SH: - case ScoreRank.S: - return Color4Extensions.FromHex(@"00a8b5"); - - case ScoreRank.A: - return Color4Extensions.FromHex(@"7cce14"); - - case ScoreRank.B: - return Color4Extensions.FromHex(@"e3b130"); - - case ScoreRank.C: - return Color4Extensions.FromHex(@"f18252"); - - default: - return Color4Extensions.FromHex(@"e95353"); - } - } - /// /// Retrieves the grade text colour. /// From e586249db70e979fd884bd050019b3a0a220f3cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:25:51 +0900 Subject: [PATCH 302/387] Expose GetRankName from DrawableRank --- osu.Game/Online/Leaderboards/DrawableRank.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 0c3ab25044..4d41230799 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -59,7 +59,7 @@ namespace osu.Game.Online.Leaderboards Padding = new MarginPadding { Top = 5 }, Colour = getRankNameColour(), Font = OsuFont.Numeric.With(size: 25), - Text = getRankName(), + Text = GetRankName(rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, @@ -69,7 +69,7 @@ namespace osu.Game.Online.Leaderboards }; } - private string getRankName() => rank.GetDescription().TrimEnd('+'); + public static string GetRankName(ScoreRank rank) => rank.GetDescription().TrimEnd('+'); /// /// Retrieves the grade text colour. From dca2e1d816971316351eec8bb266c0019abdf188 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:37:56 +0900 Subject: [PATCH 303/387] Implement the accuracy circle --- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 155 +++++++++++ .../Expanded/Accuracy/AccuracyCircle.cs | 253 ++++++++++++++++++ .../Ranking/Expanded/Accuracy/RankBadge.cs | 99 +++++++ .../Ranking/Expanded/Accuracy/RankNotch.cs | 49 ++++ .../Ranking/Expanded/Accuracy/RankText.cs | 83 ++++++ .../Accuracy/SmoothCircularProgress.cs | 126 +++++++++ 6 files changed, 765 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/SmoothCircularProgress.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs new file mode 100644 index 0000000000..d0b9d43f51 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -0,0 +1,155 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +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.Ranking.Expanded.Accuracy; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneAccuracyCircle : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(AccuracyCircle), + typeof(RankBadge), + typeof(RankNotch), + typeof(RankText), + typeof(SmoothCircularProgress) + }; + + [Test] + public void TestDRank() + { + var score = createScore(); + score.Accuracy = 0.5; + score.Rank = ScoreRank.D; + + addCircleStep(score); + } + + [Test] + public void TestCRank() + { + var score = createScore(); + score.Accuracy = 0.75; + score.Rank = ScoreRank.C; + + addCircleStep(score); + } + + [Test] + public void TestBRank() + { + var score = createScore(); + score.Accuracy = 0.85; + score.Rank = ScoreRank.B; + + addCircleStep(score); + } + + [Test] + public void TestARank() + { + var score = createScore(); + score.Accuracy = 0.925; + score.Rank = ScoreRank.A; + + addCircleStep(score); + } + + [Test] + public void TestSRank() + { + var score = createScore(); + score.Accuracy = 0.975; + score.Rank = ScoreRank.S; + + addCircleStep(score); + } + + [Test] + public void TestAlmostSSRank() + { + var score = createScore(); + score.Accuracy = 0.9999; + score.Rank = ScoreRank.S; + + addCircleStep(score); + } + + [Test] + public void TestSSRank() + { + var score = createScore(); + score.Accuracy = 1; + score.Rank = ScoreRank.X; + + addCircleStep(score); + } + + private void addCircleStep(ScoreInfo score) => AddStep("add panel", () => + { + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 700), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333")) + } + } + }, + new AccuracyCircle(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(230) + } + }; + }); + + private ScoreInfo createScore() => new ScoreInfo + { + User = new User + { + Id = 2, + Username = "peppy", + }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 2845370, + Accuracy = 0.95, + MaxCombo = 999, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs new file mode 100644 index 0000000000..873c20cc2b --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -0,0 +1,253 @@ +// 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 osu.Framework.Allocation; +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.Graphics; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// The component that displays the player's accuracy on the results screen. + /// + public class AccuracyCircle : CompositeDrawable + { + /// + /// Duration for the transforms causing this component to appear. + /// + public const double APPEAR_DURATION = 200; + + /// + /// Delay before the accuracy circle starts filling. + /// + public const double ACCURACY_TRANSFORM_DELAY = 450; + + /// + /// Duration for the accuracy circle fill. + /// + public const double ACCURACY_TRANSFORM_DURATION = 3000; + + /// + /// Delay after for the rank text (A/B/C/D/S/SS) to appear. + /// + public const double TEXT_APPEAR_DELAY = ACCURACY_TRANSFORM_DURATION / 2; + + /// + /// Delay before the rank circles start filling. + /// + public const double RANK_CIRCLE_TRANSFORM_DELAY = 150; + + /// + /// Duration for the rank circle fills. + /// + public const double RANK_CIRCLE_TRANSFORM_DURATION = 800; + + /// + /// Relative width of the rank circles. + /// + public const float RANK_CIRCLE_RADIUS = 0.06f; + + /// + /// Relative width of the circle showing the accuracy. + /// + private const float accuracy_circle_radius = 0.2f; + + /// + /// SS is displayed as a 1% region, otherwise it would be invisible. + /// + private const double virtual_ss_percentage = 0.01; + + /// + /// The easing for the circle filling transforms. + /// + public static readonly Easing ACCURACY_TRANSFORM_EASING = Easing.OutPow10; + + private readonly ScoreInfo score; + + private SmoothCircularProgress accuracyCircle; + private SmoothCircularProgress innerMask; + private Container badges; + private RankText rankText; + + public AccuracyCircle(ScoreInfo score) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new SmoothCircularProgress + { + Name = "Background circle", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(47), + Alpha = 0.5f, + InnerRadius = accuracy_circle_radius + 0.01f, // Extends a little bit into the circle + Current = { Value = 1 }, + }, + accuracyCircle = new SmoothCircularProgress + { + Name = "Accuracy circle", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#7CF6FF"), Color4Extensions.FromHex("#BAFFA9")), + InnerRadius = accuracy_circle_radius, + }, + new BufferedContainer + { + Name = "Graded circles", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Padding = new MarginPadding(2), + Children = new Drawable[] + { + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#BE0089"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 1 } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#0096A2"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 1 - virtual_ss_percentage } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#72C904"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 0.95f } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#D99D03"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 0.9f } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#EA7948"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 0.8f } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#FF5858"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 0.7f } + }, + new RankNotch(0), + new RankNotch((float)(1 - virtual_ss_percentage)), + new RankNotch(0.95f), + new RankNotch(0.9f), + new RankNotch(0.8f), + new RankNotch(0.7f), + new BufferedContainer + { + Name = "Graded circle mask", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(1), + Blending = new BlendingParameters + { + Source = BlendingType.DstColor, + Destination = BlendingType.OneMinusSrcAlpha, + SourceAlpha = BlendingType.One, + DestinationAlpha = BlendingType.SrcAlpha + }, + Child = innerMask = new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = RANK_CIRCLE_RADIUS - 0.01f, + } + } + } + }, + badges = new Container + { + Name = "Rank badges", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = -15, Horizontal = -20 }, + Children = new[] + { + new RankBadge(1f, ScoreRank.X), + new RankBadge(0.95f, ScoreRank.S), + new RankBadge(0.9f, ScoreRank.A), + new RankBadge(0.8f, ScoreRank.B), + new RankBadge(0.7f, ScoreRank.C), + } + }, + rankText = new RankText(score.Rank) + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.ScaleTo(0).Then().ScaleTo(1, APPEAR_DURATION, Easing.OutQuint); + + using (BeginDelayedSequence(RANK_CIRCLE_TRANSFORM_DELAY, true)) + innerMask.FillTo(1f, RANK_CIRCLE_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); + + using (BeginDelayedSequence(ACCURACY_TRANSFORM_DELAY, true)) + { + double targetAccuracy = score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH ? 1 : Math.Min(1 - virtual_ss_percentage, score.Accuracy); + + accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); + + foreach (var badge in badges) + { + if (badge.Accuracy > score.Accuracy) + continue; + + using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, badge.Accuracy / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) + badge.Appear(); + } + + using (BeginDelayedSequence(TEXT_APPEAR_DELAY, true)) + rankText.Appear(); + } + } + + private double inverseEasing(Easing easing, double targetValue) + { + double test = 0; + double result = 0; + int count = 2; + + while (Math.Abs(result - targetValue) > 0.005) + { + int dir = Math.Sign(targetValue - result); + + test += dir * 1.0 / count; + result = Interpolation.ApplyEasing(easing, test); + + count++; + } + + return test; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs new file mode 100644 index 0000000000..76cd408daa --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -0,0 +1,99 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// Contains a that is positioned around the . + /// + public class RankBadge : CompositeDrawable + { + /// + /// The accuracy value corresponding to the displayed by this badge. + /// + public readonly float Accuracy; + + private readonly ScoreRank rank; + + private Drawable rankContainer; + private Drawable overlay; + + /// + /// Creates a new . + /// + /// The accuracy value corresponding to . + /// The to be displayed in this . + public RankBadge(float accuracy, ScoreRank rank) + { + Accuracy = accuracy; + this.rank = rank; + + RelativeSizeAxes = Axes.Both; + Alpha = 0; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = rankContainer = new Container + { + Origin = Anchor.Centre, + Size = new Vector2(28, 14), + Children = new[] + { + new DrawableRank(rank), + overlay = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = OsuColour.ForRank(rank).Opacity(0.2f), + Radius = 10, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + } + } + } + }; + } + + /// + /// Shows this . + /// + public void Appear() + { + this.FadeIn(50); + overlay.FadeIn().FadeOut(500, Easing.In); + } + + protected override void Update() + { + base.Update(); + + // Starts at -90deg (top) and moves counter-clockwise by the accuracy + rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - Accuracy) * MathF.PI * 2); + } + + private Vector2 circlePosition(float t) + => DrawSize / 2 + new Vector2(MathF.Cos(t), MathF.Sin(t)) * DrawSize / 2; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs new file mode 100644 index 0000000000..894790b5b6 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs @@ -0,0 +1,49 @@ +// 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; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// A solid "notch" of the that appears at the ends of the rank circles to add separation. + /// + public class RankNotch : CompositeDrawable + { + private readonly float position; + + public RankNotch(float position) + { + this.position = position; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Rotation = position * 360f, + Child = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Height = AccuracyCircle.RANK_CIRCLE_RADIUS, + Width = 1f, + Colour = OsuColour.Gray(0.3f), + EdgeSmoothness = new Vector2(1f) + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs new file mode 100644 index 0000000000..b803fe6022 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs @@ -0,0 +1,83 @@ +// 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.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// The text that appears in the middle of the displaying the user's rank. + /// + public class RankText : CompositeDrawable + { + private readonly ScoreRank rank; + + private Drawable flash; + + public RankText(ScoreRank rank) + { + this.rank = rank; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Alpha = 0; + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new[] + { + new GlowingSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-15, 0), + Text = DrawableRank.GetRankName(rank), + Font = OsuFont.Numeric.With(size: 76), + UseFullGlyphHeight = false + }, + flash = new BufferedContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BlurSigma = new Vector2(35), + BypassAutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Size = new Vector2(2f), + Scale = new Vector2(1.8f), + Children = new[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-15, 0), + Text = DrawableRank.GetRankName(rank), + Font = OsuFont.Numeric.With(size: 76), + UseFullGlyphHeight = false, + Shadow = false + }, + }, + }, + }; + } + + public void Appear() + { + this.FadeIn(0, Easing.In); + + flash.FadeIn(0, Easing.In).Then().FadeOut(800, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/SmoothCircularProgress.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/SmoothCircularProgress.cs new file mode 100644 index 0000000000..106af31cae --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/SmoothCircularProgress.cs @@ -0,0 +1,126 @@ +// 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.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// Contains a with smoothened edges. + /// + public class SmoothCircularProgress : CompositeDrawable + { + public Bindable Current + { + get => progress.Current; + set => progress.Current = value; + } + + public float InnerRadius + { + get => progress.InnerRadius; + set + { + progress.InnerRadius = value; + innerSmoothingContainer.Size = new Vector2(1 - value); + smoothingWedge.Height = value / 2; + } + } + + private readonly CircularProgress progress; + private readonly Container innerSmoothingContainer; + private readonly Drawable smoothingWedge; + + public SmoothCircularProgress() + { + Container smoothingWedgeContainer; + + InternalChild = new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + progress = new CircularProgress { RelativeSizeAxes = Axes.Both }, + smoothingWedgeContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = smoothingWedge = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = 1f, + EdgeSmoothness = new Vector2(2, 0), + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-1), + Child = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + BorderThickness = 2, + Masking = true, + BorderColour = OsuColour.Gray(0.5f).Opacity(0.75f), + Blending = new BlendingParameters + { + AlphaEquation = BlendingEquation.ReverseSubtract, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + }, + innerSmoothingContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = Vector2.Zero, + Padding = new MarginPadding(-1), + Child = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + BorderThickness = 2, + BorderColour = OsuColour.Gray(0.5f).Opacity(0.75f), + Masking = true, + Blending = new BlendingParameters + { + AlphaEquation = BlendingEquation.ReverseSubtract, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + }, + } + }; + + Current.BindValueChanged(c => + { + smoothingWedgeContainer.Alpha = c.NewValue > 0 ? 1 : 0; + smoothingWedgeContainer.Rotation = (float)(360 * c.NewValue); + }, true); + } + + public TransformSequence FillTo(double newValue, double duration = 0, Easing easing = Easing.None) + => progress.FillTo(newValue, duration, easing); + } +} From daa5e63d0d40e79787fed6d5729c9382854808b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:42:55 +0900 Subject: [PATCH 304/387] Fix replay scores not being populated via player --- osu.Game/Screens/Play/Player.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bcadba14af..79f92c3762 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -401,14 +401,18 @@ namespace osu.Game.Screens.Play protected virtual ScoreInfo CreateScore() { - var score = DrawableRuleset.ReplayScore?.ScoreInfo ?? new ScoreInfo + var score = new ScoreInfo { Beatmap = Beatmap.Value.BeatmapInfo, Ruleset = rulesetInfo, Mods = Mods.Value.ToArray(), - User = api.LocalUser.Value, }; + if (DrawableRuleset.ReplayScore != null) + score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + else + score.User = api.LocalUser.Value; + ScoreProcessor.PopulateScore(score); return score; From d322c8c2d76b9030838cdaf3ad8a0543699ae539 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 16:47:23 +0900 Subject: [PATCH 305/387] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 66a1523843..942970c890 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 647f05b428..54f1ad2845 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 0e5c64cf0f..816a430b52 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 05789e6fe4c91b20edb3ce3bcdb3a8c4e6c986d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:59:34 +0900 Subject: [PATCH 306/387] Implement the score panel --- .../Visual/Ranking/TestSceneScorePanel.cs | 143 +++++++++++ .../Expanded/ExpandedPanelMiddleContent.cs | 15 ++ .../Expanded/ExpandedPanelTopContent.cs | 15 ++ osu.Game/Screens/Ranking/PanelState.cs | 11 + osu.Game/Screens/Ranking/ScorePanel.cs | 223 ++++++++++++++++++ 5 files changed, 407 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs create mode 100644 osu.Game/Screens/Ranking/PanelState.cs create mode 100644 osu.Game/Screens/Ranking/ScorePanel.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs new file mode 100644 index 0000000000..1e55885385 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -0,0 +1,143 @@ +// 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.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.Ranking; +using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneScorePanel : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ScorePanel), + typeof(PanelState), + typeof(ExpandedPanelMiddleContent), + typeof(ExpandedPanelTopContent), + }; + + [Test] + public void TestDRank() + { + var score = createScore(); + score.Accuracy = 0.5; + score.Rank = ScoreRank.D; + + addPanelStep(score); + } + + [Test] + public void TestCRank() + { + var score = createScore(); + score.Accuracy = 0.75; + score.Rank = ScoreRank.C; + + addPanelStep(score); + } + + [Test] + public void TestBRank() + { + var score = createScore(); + score.Accuracy = 0.85; + score.Rank = ScoreRank.B; + + addPanelStep(score); + } + + [Test] + public void TestARank() + { + var score = createScore(); + score.Accuracy = 0.925; + score.Rank = ScoreRank.A; + + addPanelStep(score); + } + + [Test] + public void TestSRank() + { + var score = createScore(); + score.Accuracy = 0.975; + score.Rank = ScoreRank.S; + + addPanelStep(score); + } + + [Test] + public void TestAlmostSSRank() + { + var score = createScore(); + score.Accuracy = 0.9999; + score.Rank = ScoreRank.S; + + addPanelStep(score); + } + + [Test] + public void TestSSRank() + { + var score = createScore(); + score.Accuracy = 1; + score.Rank = ScoreRank.X; + + addPanelStep(score); + } + + [Test] + public void TestAllHitResults() + { + var score = createScore(); + score.Statistics[HitResult.Perfect] = 350; + score.Statistics[HitResult.Ok] = 200; + + addPanelStep(score); + } + + private void addPanelStep(ScoreInfo score) => AddStep("add panel", () => + { + Child = new ScorePanel(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = PanelState.Expanded + }; + }); + + private ScoreInfo createScore() => new ScoreInfo + { + User = new User + { + Id = 2, + Username = "peppy", + }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 2845370, + Accuracy = 0.95, + MaxCombo = 999, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs new file mode 100644 index 0000000000..c41829051a --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -0,0 +1,15 @@ +// 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.Containers; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Ranking.Expanded +{ + public class ExpandedPanelMiddleContent : CompositeDrawable + { + public ExpandedPanelMiddleContent(ScoreInfo score) + { + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs new file mode 100644 index 0000000000..064d1ed7b9 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs @@ -0,0 +1,15 @@ +// 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.Containers; +using osu.Game.Users; + +namespace osu.Game.Screens.Ranking.Expanded +{ + public class ExpandedPanelTopContent : CompositeDrawable + { + public ExpandedPanelTopContent(User user) + { + } + } +} diff --git a/osu.Game/Screens/Ranking/PanelState.cs b/osu.Game/Screens/Ranking/PanelState.cs new file mode 100644 index 0000000000..94e2c7cef4 --- /dev/null +++ b/osu.Game/Screens/Ranking/PanelState.cs @@ -0,0 +1,11 @@ +// 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.Ranking +{ + public enum PanelState + { + Expanded, + Contracted + } +} diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs new file mode 100644 index 0000000000..a1adfcc500 --- /dev/null +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -0,0 +1,223 @@ +// 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 osu.Framework; +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.Framework.Graphics.Shapes; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Expanded; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Ranking +{ + public class ScorePanel : CompositeDrawable, IStateful + { + /// + /// Width of the panel when contracted. + /// + private const float contracted_width = 160; + + /// + /// Height of the panel when contracted. + /// + private const float contracted_height = 320; + + /// + /// Width of the panel when expanded. + /// + private const float expanded_width = 360; + + /// + /// Height of the panel when expanded. + /// + private const float expanded_height = 560; + + /// + /// Height of the top layer when the panel is expanded. + /// + private const float expanded_top_layer_height = 53; + + /// + /// Height of the top layer when the panel is contracted. + /// + private const float contracted_top_layer_height = 40; + + /// + /// Duration for the panel to resize into its expanded/contracted size. + /// + private const double resize_duration = 200; + + /// + /// Delay after before the top layer is expanded. + /// + private const double top_layer_expand_delay = 100; + + /// + /// Duration for the top layer expansion. + /// + private const double top_layer_expand_duration = 200; + + /// + /// Duration for the panel contents to fade in. + /// + private const double content_fade_duration = 50; + + private static readonly ColourInfo expanded_top_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#444"), Color4Extensions.FromHex("#333")); + private static readonly ColourInfo expanded_middle_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333")); + private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); + private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#444"); + + public event Action StateChanged; + + private readonly ScoreInfo score; + + private Container topLayerContainer; + private Drawable topLayerBackground; + private Container topLayerContentContainer; + private Drawable topLayerContent; + + private Container middleLayerContainer; + private Drawable middleLayerBackground; + private Container middleLayerContentContainer; + private Drawable middleLayerContent; + + public ScorePanel(ScoreInfo score) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + topLayerContainer = new Container + { + Name = "Top layer", + RelativeSizeAxes = Axes.X, + Height = 120, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + } + }, + middleLayerContainer = new Container + { + Name = "Middle layer", + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (state == PanelState.Expanded) + { + topLayerBackground.FadeColour(expanded_top_layer_colour); + middleLayerBackground.FadeColour(expanded_middle_layer_colour); + } + else + { + topLayerBackground.FadeColour(contracted_top_layer_colour); + middleLayerBackground.FadeColour(contracted_middle_layer_colour); + } + + updateState(); + } + + private PanelState state = PanelState.Contracted; + + public PanelState State + { + get => state; + set + { + if (state == value) + return; + + state = value; + + if (LoadState >= LoadState.Ready) + updateState(); + + StateChanged?.Invoke(value); + } + } + + private void updateState() + { + topLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint); + middleLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint); + + topLayerContent?.FadeOut(content_fade_duration).Expire(); + middleLayerContent?.FadeOut(content_fade_duration).Expire(); + + switch (state) + { + case PanelState.Expanded: + this.ResizeTo(new Vector2(expanded_width, expanded_height), resize_duration, Easing.OutQuint); + + topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); + middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); + + topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(score.User).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(score).With(d => d.Alpha = 0)); + break; + + case PanelState.Contracted: + this.ResizeTo(new Vector2(contracted_width, contracted_height), resize_duration, Easing.OutQuint); + + topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint); + middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint); + break; + } + + using (BeginDelayedSequence(resize_duration + top_layer_expand_delay, true)) + { + switch (state) + { + case PanelState.Expanded: + topLayerContainer.MoveToY(-expanded_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); + middleLayerContainer.MoveToY(expanded_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); + break; + + case PanelState.Contracted: + topLayerContainer.MoveToY(-contracted_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); + middleLayerContainer.MoveToY(contracted_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); + break; + } + + topLayerContent?.FadeIn(content_fade_duration); + middleLayerContent?.FadeIn(content_fade_duration); + } + } + } +} From 7cc1a6040fca3fff5f6870252662c3f4e575086c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:01:38 +0900 Subject: [PATCH 307/387] Implement top panel contents --- .../TestSceneExpandedPanelTopContent.cs | 35 ++++++++++++++++ .../Expanded/ExpandedPanelTopContent.cs | 42 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs new file mode 100644 index 0000000000..afaa607099 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs @@ -0,0 +1,35 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneExpandedPanelTopContent : OsuTestScene + { + public TestSceneExpandedPanelTopContent() + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#444"), + }, + new ExpandedPanelTopContent(new User { Id = 2, Username = "peppy" }), + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs index 064d1ed7b9..a9853c217c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs @@ -1,15 +1,57 @@ // 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.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Screens.Ranking.Expanded { public class ExpandedPanelTopContent : CompositeDrawable { + private readonly User user; + public ExpandedPanelTopContent(User user) { + this.user = user; + Anchor = Anchor.TopCentre; + Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new UpdateableAvatar(user) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(80), + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = user.Username, + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold) + } + } + }; } } } From e56d0f2eeaaed4fce4faded742b5fdb6ac3b93d2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:16:10 +0900 Subject: [PATCH 308/387] Add black font weighting --- osu.Game/Graphics/OsuFont.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 255f7f24f7..7c78141b4d 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -136,5 +136,10 @@ namespace osu.Game.Graphics /// Equivalent to weight 700. /// Bold = 700, + + /// + /// Equivalent to weight 900. + /// + Black = 900 } } From 1521f25c96eec4c30119252d7a51311794a1c580 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:25:24 +0900 Subject: [PATCH 309/387] Implement middle panel contents --- .../TestSceneExpandedPanelMiddleContent.cs | 80 +++++++ .../Expanded/ExpandedPanelMiddleContent.cs | 204 ++++++++++++++++++ .../Ranking/Expanded/StarRatingDisplay.cs | 95 ++++++++ .../Expanded/Statistics/AccuracyStatistic.cs | 51 +++++ .../Expanded/Statistics/ComboStatistic.cs | 66 ++++++ .../Expanded/Statistics/CounterStatistic.cs | 48 +++++ .../Expanded/Statistics/StatisticDisplay.cs | 82 +++++++ .../Ranking/Expanded/TotalScoreCounter.cs | 35 +++ 8 files changed, 661 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs new file mode 100644 index 0000000000..665b3ad455 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -0,0 +1,80 @@ +// 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 osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +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.Ranking.Expanded; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osu.Game.Screens.Ranking.Expanded.Statistics; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneExpandedPanelMiddleContent : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ExpandedPanelMiddleContent), + typeof(AccuracyCircle), + typeof(AccuracyStatistic), + typeof(ComboStatistic), + typeof(CounterStatistic), + typeof(StarRatingDisplay), + typeof(StatisticDisplay), + typeof(TotalScoreCounter) + }; + + public TestSceneExpandedPanelMiddleContent() + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 700), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#444"), + }, + new ExpandedPanelMiddleContent(createTestScore()) + } + }; + } + + private ScoreInfo createTestScore() => new ScoreInfo + { + User = new User + { + Id = 2, + Username = "peppy", + }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 999999, + Accuracy = 0.95, + MaxCombo = 999, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index c41829051a..4f45b1c5d7 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -1,15 +1,219 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osu.Game.Screens.Ranking.Expanded.Statistics; +using osuTK; namespace osu.Game.Screens.Ranking.Expanded { public class ExpandedPanelMiddleContent : CompositeDrawable { + private readonly ScoreInfo score; + + private readonly List statisticDisplays = new List(); + private RollingCounter scoreCounter; + public ExpandedPanelMiddleContent(ScoreInfo score) { + this.score = score; + + RelativeSizeAxes = Axes.Both; + Masking = true; + + Padding = new MarginPadding { Vertical = 10, Horizontal = 10 }; + } + + [BackgroundDependencyLoader] + private void load() + { + var topStatistics = new List + { + new AccuracyStatistic(score.Accuracy), + new ComboStatistic(score.MaxCombo, true), + new CounterStatistic("pp", (int)(score.PP ?? 0)), + }; + + var bottomStatistics = new List(); + foreach (var stat in score.SortedStatistics) + bottomStatistics.Add(new CounterStatistic(stat.Key.GetDescription(), stat.Value)); + + statisticDisplays.AddRange(topStatistics); + statisticDisplays.AddRange(bottomStatistics); + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new LocalisedString((score.Beatmap.Metadata.Title, score.Beatmap.Metadata.TitleUnicode)), + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new LocalisedString((score.Beatmap.Metadata.Artist, score.Beatmap.Metadata.ArtistUnicode)), + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold) + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = 40 }, + RelativeSizeAxes = Axes.X, + Height = 230, + Child = new AccuracyCircle(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + } + }, + scoreCounter = new TotalScoreCounter + { + Margin = new MarginPadding { Top = 0, Bottom = 5 }, + Current = { Value = 0 }, + Alpha = 0, + AlwaysPresent = true + }, + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new StarRatingDisplay(score.Beatmap) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DisplayUnrankedText = false, + ExpansionMode = ExpansionMode.AlwaysExpanded, + Scale = new Vector2(0.5f), + Current = { Value = score.Mods } + } + } + }, + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = score.Beatmap.Version, + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + }, + new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }.With(t => + { + t.AddText("mapped by "); + t.AddText(score.UserString, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + }) + } + }, + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { topStatistics.Cast().ToArray() }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { bottomStatistics.Cast().ToArray() }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Score counter value setting must be scheduled so it isn't transferred instantaneously + ScheduleAfterChildren(() => + { + using (BeginDelayedSequence(AccuracyCircle.ACCURACY_TRANSFORM_DELAY, true)) + { + scoreCounter.FadeIn(); + scoreCounter.Current.Value = score.TotalScore; + + double delay = 0; + + foreach (var stat in statisticDisplays) + { + using (BeginDelayedSequence(delay, true)) + stat.Appear(); + + delay += 200; + } + } + }); } } } diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs new file mode 100644 index 0000000000..87d9828707 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Ranking.Expanded +{ + public class StarRatingDisplay : CompositeDrawable + { + private readonly BeatmapInfo beatmap; + + public StarRatingDisplay(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + var starRatingParts = beatmap.StarDifficulty.ToString("0.00", CultureInfo.InvariantCulture).Split('.'); + string wholePart = starRatingParts[0]; + string fractionPart = starRatingParts[1]; + string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; + + InternalChildren = new Drawable[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.ForDifficultyRating(beatmap.DifficultyRating) + }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 8, Vertical = 4 }, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2, 0), + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(7), + Icon = FontAwesome.Solid.Star, + Colour = Color4.Black + }, + new OsuTextFlowContainer(s => s.Font = OsuFont.Numeric.With(weight: FontWeight.Black)) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + TextAnchor = Anchor.BottomLeft, + }.With(t => + { + t.AddText($"{wholePart}", s => + { + s.Colour = Color4.Black; + s.Font = s.Font.With(size: 14); + s.UseFullGlyphHeight = false; + }); + + t.AddText($"{separator}{fractionPart}", s => + { + s.Colour = Color4.Black; + s.Font = s.Font.With(size: 7); + s.UseFullGlyphHeight = false; + }); + }) + } + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs new file mode 100644 index 0000000000..2f7fc3a4fd --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -0,0 +1,51 @@ +// 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.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class AccuracyStatistic : StatisticDisplay + { + private readonly double accuracy; + + private RollingCounter counter; + + public AccuracyStatistic(double accuracy) + : base("accuracy") + { + this.accuracy = accuracy; + } + + public override void Appear() + { + base.Appear(); + counter.Current.Value = accuracy; + } + + protected override Drawable CreateContent() => counter = new Counter(); + + private class Counter : RollingCounter + { + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; + + protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + + public Counter() + { + DisplayedCountSpriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + DisplayedCountSpriteText.Spacing = new Vector2(-2, 0); + } + + protected override string FormatCount(double count) => count.FormatAccuracy(); + + public override void Increment(double amount) + => Current.Value += amount; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs new file mode 100644 index 0000000000..ce5a15da01 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs @@ -0,0 +1,66 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class ComboStatistic : CounterStatistic + { + private readonly bool isPerfect; + + private Drawable perfectText; + + public ComboStatistic(int combo, bool isPerfect) + : base("combo", combo) + { + this.isPerfect = isPerfect; + } + + public override void Appear() + { + base.Appear(); + + if (isPerfect) + { + using (BeginDelayedSequence(AccuracyCircle.ACCURACY_TRANSFORM_DURATION / 2, true)) + perfectText.FadeIn(50); + } + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new[] + { + base.CreateContent().With(d => + { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + }), + perfectText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "PERFECT", + Font = OsuFont.Torus.With(size: 11, weight: FontWeight.SemiBold), + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66FFCC"), Color4Extensions.FromHex("#FF9AD7")), + Alpha = 0, + UseFullGlyphHeight = false, + } + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs new file mode 100644 index 0000000000..ee07ea326d --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -0,0 +1,48 @@ +// 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.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class CounterStatistic : StatisticDisplay + { + private readonly int count; + + private RollingCounter counter; + + public CounterStatistic(string header, int count) + : base(header) + { + this.count = count; + } + + public override void Appear() + { + base.Appear(); + counter.Current.Value = count; + } + + protected override Drawable CreateContent() => counter = new Counter(); + + private class Counter : RollingCounter + { + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; + + protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + + public Counter() + { + DisplayedCountSpriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + DisplayedCountSpriteText.Spacing = new Vector2(-2, 0); + } + + public override void Increment(int amount) + => Current.Value += amount; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs new file mode 100644 index 0000000000..55015b432b --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs @@ -0,0 +1,82 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public abstract class StatisticDisplay : CompositeDrawable + { + private readonly string header; + + private Drawable content; + + protected StatisticDisplay(string header) + { + this.header = header; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.X, + Height = 12, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#222") + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + Text = header.ToUpperInvariant(), + } + } + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Children = new[] + { + content = CreateContent().With(d => + { + d.Anchor = Anchor.TopCentre; + d.Origin = Anchor.TopCentre; + d.Alpha = 0; + d.AlwaysPresent = true; + }), + } + } + } + }; + } + + public virtual void Appear() => content.FadeIn(100); + + protected abstract Drawable CreateContent(); + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs new file mode 100644 index 0000000000..d230e56649 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs @@ -0,0 +1,35 @@ +// 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.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded +{ + public class TotalScoreCounter : RollingCounter + { + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; + + protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + + public TotalScoreCounter() + { + // Todo: AutoSize X removed here due to https://github.com/ppy/osu-framework/issues/3369 + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + DisplayedCountSpriteText.Anchor = Anchor.TopCentre; + DisplayedCountSpriteText.Origin = Anchor.TopCentre; + + DisplayedCountSpriteText.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true); + DisplayedCountSpriteText.Spacing = new Vector2(-5, 0); + } + + protected override string FormatCount(long count) => count.ToString("N0"); + + public override void Increment(long amount) + => Current.Value += amount; + } +} From 2ee480f1d8aaaec127856a7f0a8234238c9391fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:34:16 +0900 Subject: [PATCH 310/387] Add xmldocs / cleanup --- .../Expanded/ExpandedPanelMiddleContent.cs | 7 +++ .../Ranking/Expanded/StarRatingDisplay.cs | 7 +++ .../Expanded/Statistics/AccuracyStatistic.cs | 7 +++ .../Expanded/Statistics/ComboStatistic.cs | 51 ++++++++++--------- .../Expanded/Statistics/CounterStatistic.cs | 8 +++ .../Expanded/Statistics/StatisticDisplay.cs | 13 +++++ .../Ranking/Expanded/TotalScoreCounter.cs | 3 ++ 7 files changed, 73 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 4f45b1c5d7..6d5d7e0d95 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -20,6 +20,9 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded { + /// + /// The content that appears in the middle section of the . + /// public class ExpandedPanelMiddleContent : CompositeDrawable { private readonly ScoreInfo score; @@ -27,6 +30,10 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly List statisticDisplays = new List(); private RollingCounter scoreCounter; + /// + /// Creates a new . + /// + /// The score to display. public ExpandedPanelMiddleContent(ScoreInfo score) { this.score = score; diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs index 87d9828707..74b58b9f8c 100644 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -15,10 +15,17 @@ using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded { + /// + /// A pill that displays the star rating of a . + /// public class StarRatingDisplay : CompositeDrawable { private readonly BeatmapInfo beatmap; + /// + /// Creates a new . + /// + /// The to display the star difficulty of. public StarRatingDisplay(BeatmapInfo beatmap) { this.beatmap = beatmap; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index 2f7fc3a4fd..2a0e33aab7 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -10,12 +10,19 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics { + /// + /// A to display the player's accuracy. + /// public class AccuracyStatistic : StatisticDisplay { private readonly double accuracy; private RollingCounter counter; + /// + /// Creates a new . + /// + /// The accuracy to display. public AccuracyStatistic(double accuracy) : base("accuracy") { diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs index ce5a15da01..e13138c5a0 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs @@ -12,12 +12,20 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics { + /// + /// A to display the player's combo. + /// public class ComboStatistic : CounterStatistic { private readonly bool isPerfect; private Drawable perfectText; + /// + /// Creates a new . + /// + /// The combo to be displayed. + /// Whether this is a perfect combo. public ComboStatistic(int combo, bool isPerfect) : base("combo", combo) { @@ -35,32 +43,29 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } } - protected override Drawable CreateContent() + protected override Drawable CreateContent() => new FillFlowContainer { - return new FillFlowContainer + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new[] + base.CreateContent().With(d => { - base.CreateContent().With(d => - { - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - }), - perfectText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = "PERFECT", - Font = OsuFont.Torus.With(size: 11, weight: FontWeight.SemiBold), - Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66FFCC"), Color4Extensions.FromHex("#FF9AD7")), - Alpha = 0, - UseFullGlyphHeight = false, - } + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + }), + perfectText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "PERFECT", + Font = OsuFont.Torus.With(size: 11, weight: FontWeight.SemiBold), + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66FFCC"), Color4Extensions.FromHex("#FF9AD7")), + Alpha = 0, + UseFullGlyphHeight = false, } - }; - } + } + }; } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index ee07ea326d..817cc9b8c2 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -9,12 +9,20 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics { + /// + /// A to display general numeric values. + /// public class CounterStatistic : StatisticDisplay { private readonly int count; private RollingCounter counter; + /// + /// Creates a new . + /// + /// The name of the statistic. + /// The value to display. public CounterStatistic(string header, int count) : base(header) { diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs index 55015b432b..a653cc82d4 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs @@ -11,12 +11,19 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Screens.Ranking.Expanded.Statistics { + /// + /// A statistic from the score to be displayed in the . + /// public abstract class StatisticDisplay : CompositeDrawable { private readonly string header; private Drawable content; + /// + /// Creates a new . + /// + /// The name of the statistic. protected StatisticDisplay(string header) { this.header = header; @@ -75,8 +82,14 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics }; } + /// + /// Shows the statistic value. + /// public virtual void Appear() => content.FadeIn(100); + /// + /// Creates the content for this . + /// protected abstract Drawable CreateContent(); } } diff --git a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs index d230e56649..cab04edb8b 100644 --- a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs +++ b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs @@ -9,6 +9,9 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded { + /// + /// A counter for the player's total score to be displayed in the . + /// public class TotalScoreCounter : RollingCounter { protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; From 6f801e1695c632a9a703ced501f4814f63ffacec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:35:14 +0900 Subject: [PATCH 311/387] Add xmldoc --- .../Screens/Ranking/Expanded/ExpandedPanelTopContent.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs index a9853c217c..5dfc43cc29 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs @@ -12,10 +12,17 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded { + /// + /// The content that appears in the middle section of the . + /// public class ExpandedPanelTopContent : CompositeDrawable { private readonly User user; + /// + /// Creates a new . + /// + /// The to display. public ExpandedPanelTopContent(User user) { this.user = user; From 1c4296f5e7826689fd6ce4f483e13cafcea21988 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:43:16 +0900 Subject: [PATCH 312/387] Implement the new results screen --- .../Background/TestSceneUserDimBackgrounds.cs | 5 +- .../Gameplay/TestSceneReplayDownloadButton.cs | 2 +- .../Multiplayer/TestSceneMatchResults.cs | 106 ----- .../TestSceneResultsScreen.cs} | 15 +- .../Screens/Multi/Play/TimeshiftPlayer.cs | 4 - .../Screens/Multi/Ranking/MatchResults.cs | 26 -- .../Ranking/Pages/RoomLeaderboardPage.cs | 135 ------ .../Ranking/Types/RoomLeaderboardPageInfo.cs | 29 -- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/SoloResults.cs | 24 - osu.Game/Screens/Ranking/IResultPageInfo.cs | 16 - .../Ranking/Pages/LocalLeaderboardPage.cs | 43 -- .../Screens/Ranking/Pages/ScoreResultsPage.cs | 428 ------------------ .../{Pages => }/ReplayDownloadButton.cs | 2 +- osu.Game/Screens/Ranking/ResultModeButton.cs | 97 ---- .../Screens/Ranking/ResultModeTabControl.cs | 30 -- osu.Game/Screens/Ranking/Results.cs | 291 ------------ osu.Game/Screens/Ranking/ResultsPage.cs | 92 ---- osu.Game/Screens/Ranking/ResultsScreen.cs | 97 ++++ .../Ranking/{Pages => }/RetryButton.cs | 2 +- .../Ranking/Types/LocalLeaderboardPageInfo.cs | 28 -- .../Ranking/Types/ScoreOverviewPageInfo.cs | 30 -- osu.Game/Screens/Select/PlaySongSelect.cs | 3 +- 23 files changed, 113 insertions(+), 1394 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs rename osu.Game.Tests/Visual/{Gameplay/TestSceneResults.cs => Ranking/TestSceneResultsScreen.cs} (91%) delete mode 100644 osu.Game/Screens/Multi/Ranking/MatchResults.cs delete mode 100644 osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs delete mode 100644 osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs delete mode 100644 osu.Game/Screens/Play/SoloResults.cs delete mode 100644 osu.Game/Screens/Ranking/IResultPageInfo.cs delete mode 100644 osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs delete mode 100644 osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs rename osu.Game/Screens/Ranking/{Pages => }/ReplayDownloadButton.cs (98%) delete mode 100644 osu.Game/Screens/Ranking/ResultModeButton.cs delete mode 100644 osu.Game/Screens/Ranking/ResultModeTabControl.cs delete mode 100644 osu.Game/Screens/Ranking/Results.cs delete mode 100644 osu.Game/Screens/Ranking/ResultsPage.cs create mode 100644 osu.Game/Screens/Ranking/ResultsScreen.cs rename osu.Game/Screens/Ranking/{Pages => }/RetryButton.cs (97%) delete mode 100644 osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs delete mode 100644 osu.Game/Screens/Ranking/Types/ScoreOverviewPageInfo.cs diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 06a155e78b..b51555db3e 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -27,6 +27,7 @@ using osu.Game.Screens; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Tests.Resources; using osu.Game.Users; @@ -203,7 +204,7 @@ namespace osu.Game.Tests.Visual.Background } /// - /// Check if the visual settings container removes user dim when suspending for + /// Check if the visual settings container removes user dim when suspending for /// [Test] public void TransitionTest() @@ -335,7 +336,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsBackgroundCurrent() => ((FadeAccessibleBackground)Background).IsCurrentScreen(); } - private class FadeAccessibleResults : SoloResults + private class FadeAccessibleResults : ResultsScreen { public FadeAccessibleResults(ScoreInfo score) : base(score) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 8cb44de8cb..c9561a70fa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -11,7 +11,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Rulesets; -using osu.Game.Screens.Ranking.Pages; +using osu.Game.Screens.Ranking; namespace osu.Game.Tests.Visual.Gameplay { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs deleted file mode 100644 index 58e9240026..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs +++ /dev/null @@ -1,106 +0,0 @@ -// 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 System.Linq; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Scoring; -using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Multi.Ranking; -using osu.Game.Screens.Multi.Ranking.Pages; -using osu.Game.Screens.Multi.Ranking.Types; -using osu.Game.Screens.Ranking; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestSceneMatchResults : MultiplayerTestScene - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(MatchResults), - typeof(RoomLeaderboardPageInfo), - typeof(RoomLeaderboardPage) - }; - - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); - if (beatmapInfo != null) - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); - - Room.RoomID.Value = 1; - Room.Name.Value = "an awesome room"; - - LoadScreen(new TestMatchResults(new ScoreInfo - { - User = new User { Id = 10 }, - })); - } - - private class TestMatchResults : MatchResults - { - public TestMatchResults(ScoreInfo score) - : base(score) - { - } - - protected override IEnumerable CreateResultPages() => new[] { new TestRoomLeaderboardPageInfo(Score, Beatmap.Value) }; - } - - private class TestRoomLeaderboardPageInfo : RoomLeaderboardPageInfo - { - private readonly ScoreInfo score; - private readonly WorkingBeatmap beatmap; - - public TestRoomLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap) - : base(score, beatmap) - { - this.score = score; - this.beatmap = beatmap; - } - - public override ResultsPage CreatePage() => new TestRoomLeaderboardPage(score, beatmap); - } - - private class TestRoomLeaderboardPage : RoomLeaderboardPage - { - public TestRoomLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap) - : base(score, beatmap) - { - } - - protected override MatchLeaderboard CreateLeaderboard() => new TestMatchLeaderboard(); - } - - private class TestMatchLeaderboard : RoomLeaderboardPage.ResultsMatchLeaderboard - { - protected override APIRequest FetchScores(Action> scoresCallback) - { - var scores = Enumerable.Range(0, 50).Select(createRoomScore).ToArray(); - - scoresCallback?.Invoke(scores); - ScoresLoaded?.Invoke(scores); - - return null; - } - - private APIUserScoreAggregate createRoomScore(int id) => new APIUserScoreAggregate - { - User = new User { Id = id, Username = $"User {id}" }, - Accuracy = 0.98, - TotalScore = 987654, - TotalAttempts = 13, - CompletedBeatmaps = 5 - }; - } - } -} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs similarity index 91% rename from osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 2b7a32ba17..bd5b039bc1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -10,29 +10,27 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Ranking.Pages; +using osu.Game.Tests.Beatmaps; using osu.Game.Users; -namespace osu.Game.Tests.Visual.Gameplay +namespace osu.Game.Tests.Visual.Ranking { [TestFixture] - public class TestSceneResults : ScreenTestScene + public class TestSceneResultsScreen : ScreenTestScene { private BeatmapManager beatmaps; public override IReadOnlyList RequiredTypes => new[] { - typeof(Results), - typeof(ResultsPage), - typeof(ScoreResultsPage), + typeof(ResultsScreen), typeof(RetryButton), typeof(ReplayDownloadButton), - typeof(LocalLeaderboardPage), typeof(TestPlayer) }; @@ -65,6 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay { HitResult.Meh, 50 }, { HitResult.Miss, 1 } }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, User = new User { Username = "peppy", @@ -119,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private class TestSoloResults : SoloResults + private class TestSoloResults : ResultsScreen { public HotkeyRetryOverlay RetryOverlay; diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 3afacf2f31..7f58de29fb 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -14,9 +14,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Play; -using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Multi.Play { @@ -115,7 +113,5 @@ namespace osu.Game.Screens.Multi.Play Exited = null; } - - protected override Results CreateResults(ScoreInfo score) => new MatchResults(score); } } diff --git a/osu.Game/Screens/Multi/Ranking/MatchResults.cs b/osu.Game/Screens/Multi/Ranking/MatchResults.cs deleted file mode 100644 index fe68d7e849..0000000000 --- a/osu.Game/Screens/Multi/Ranking/MatchResults.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking.Types; -using osu.Game.Screens.Ranking; -using osu.Game.Screens.Ranking.Types; - -namespace osu.Game.Screens.Multi.Ranking -{ - public class MatchResults : Results - { - public MatchResults(ScoreInfo score) - : base(score) - { - } - - protected override IEnumerable CreateResultPages() => new IResultPageInfo[] - { - new ScoreOverviewPageInfo(Score, Beatmap.Value), - new LocalLeaderboardPageInfo(Score, Beatmap.Value), - new RoomLeaderboardPageInfo(Score, Beatmap.Value), - }; - } -} diff --git a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs deleted file mode 100644 index 0d31805774..0000000000 --- a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Internal; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Lists; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Leaderboards; -using osu.Game.Online.Multiplayer; -using osu.Game.Scoring; -using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Ranking; - -namespace osu.Game.Screens.Multi.Ranking.Pages -{ - public class RoomLeaderboardPage : ResultsPage - { - [Resolved] - private OsuColour colours { get; set; } - - private TextFlowContainer rankText; - - [Resolved(typeof(Room), nameof(Room.Name))] - private Bindable name { get; set; } - - public RoomLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap) - : base(score, beatmap) - { - } - - [BackgroundDependencyLoader] - private void load() - { - MatchLeaderboard leaderboard; - - Children = new Drawable[] - { - new Box - { - Colour = colours.Gray6, - RelativeSizeAxes = Axes.Both, - }, - new BufferedContainer - { - RelativeSizeAxes = Axes.Both, - BackgroundColour = colours.Gray6, - Child = leaderboard = CreateLeaderboard() - }, - rankText = new TextFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Width = 0.5f, - AutoSizeAxes = Axes.Y, - Y = 50, - TextAnchor = Anchor.TopCentre - }, - }; - - leaderboard.Origin = Anchor.Centre; - leaderboard.Anchor = Anchor.Centre; - leaderboard.RelativeSizeAxes = Axes.Both; - leaderboard.Height = 0.8f; - leaderboard.Y = 55; - leaderboard.ScoresLoaded = scoresLoaded; - } - - private void scoresLoaded(IEnumerable scores) - { - void gray(SpriteText s) => s.Colour = colours.GrayC; - - void white(SpriteText s) - { - s.Font = s.Font.With(size: s.Font.Size * 1.4f); - s.Colour = colours.GrayF; - } - - rankText.AddText(name + "\n", white); - rankText.AddText("You are placed ", gray); - - int index = scores.IndexOf(new APIUserScoreAggregate { User = Score.User }, new FuncEqualityComparer((s1, s2) => s1.User.Id.Equals(s2.User.Id))); - - rankText.AddText($"#{index + 1} ", s => - { - s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold); - s.Colour = colours.YellowDark; - }); - - rankText.AddText("in the room!", gray); - } - - protected virtual MatchLeaderboard CreateLeaderboard() => new ResultsMatchLeaderboard(); - - public class ResultsMatchLeaderboard : MatchLeaderboard - { - protected override bool FadeTop => true; - - protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) - => new ResultsMatchLeaderboardScore(model, index); - - protected override FillFlowContainer CreateScoreFlow() - { - var flow = base.CreateScoreFlow(); - flow.Padding = new MarginPadding - { - Top = LeaderboardScore.HEIGHT * 2, - Bottom = LeaderboardScore.HEIGHT * 3, - }; - return flow; - } - - private class ResultsMatchLeaderboardScore : MatchLeaderboardScore - { - public ResultsMatchLeaderboardScore(APIUserScoreAggregate score, int rank) - : base(score, rank) - { - } - - [BackgroundDependencyLoader] - private void load() - { - } - } - } - } -} diff --git a/osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs b/osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs deleted file mode 100644 index dcfad8458f..0000000000 --- a/osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking.Pages; -using osu.Game.Screens.Ranking; - -namespace osu.Game.Screens.Multi.Ranking.Types -{ - public class RoomLeaderboardPageInfo : IResultPageInfo - { - private readonly ScoreInfo score; - private readonly WorkingBeatmap beatmap; - - public RoomLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap) - { - this.score = score; - this.beatmap = beatmap; - } - - public IconUsage Icon => FontAwesome.Solid.Users; - - public string Name => "Room Leaderboard"; - - public virtual ResultsPage CreatePage() => new RoomLeaderboardPage(score, beatmap); - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bcadba14af..7d945f9a31 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -416,7 +416,7 @@ namespace osu.Game.Screens.Play protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score); + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score); #region Fail Logic diff --git a/osu.Game/Screens/Play/SoloResults.cs b/osu.Game/Screens/Play/SoloResults.cs deleted file mode 100644 index 2b9aec257c..0000000000 --- a/osu.Game/Screens/Play/SoloResults.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking; -using osu.Game.Screens.Ranking.Types; - -namespace osu.Game.Screens.Play -{ - public class SoloResults : Results - { - public SoloResults(ScoreInfo score) - : base(score) - { - } - - protected override IEnumerable CreateResultPages() => new IResultPageInfo[] - { - new ScoreOverviewPageInfo(Score, Beatmap.Value), - new LocalLeaderboardPageInfo(Score, Beatmap.Value) - }; - } -} diff --git a/osu.Game/Screens/Ranking/IResultPageInfo.cs b/osu.Game/Screens/Ranking/IResultPageInfo.cs deleted file mode 100644 index cc86e7441a..0000000000 --- a/osu.Game/Screens/Ranking/IResultPageInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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.Sprites; - -namespace osu.Game.Screens.Ranking -{ - public interface IResultPageInfo - { - IconUsage Icon { get; } - - string Name { get; } - - ResultsPage CreatePage(); - } -} diff --git a/osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs b/osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs deleted file mode 100644 index c997dd6d30..0000000000 --- a/osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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.Shapes; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Scoring; -using osu.Game.Screens.Select.Leaderboards; -using osuTK; - -namespace osu.Game.Screens.Ranking.Pages -{ - public class LocalLeaderboardPage : ResultsPage - { - public LocalLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap = null) - : base(score, beatmap) - { - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Children = new Drawable[] - { - new Box - { - Colour = colours.Gray6, - RelativeSizeAxes = Axes.Both, - }, - new BeatmapLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Beatmap = Beatmap.BeatmapInfo ?? Score.Beatmap, - Scale = new Vector2(0.7f) - } - }; - } - } -} diff --git a/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs b/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs deleted file mode 100644 index 0aab067de1..0000000000 --- a/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs +++ /dev/null @@ -1,428 +0,0 @@ -// 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 System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Leaderboards; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Screens.Play; -using osu.Game.Users; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Ranking.Pages -{ - public class ScoreResultsPage : ResultsPage - { - private Container scoreContainer; - private ScoreCounter scoreCounter; - - private readonly ScoreInfo score; - - public ScoreResultsPage(ScoreInfo score, WorkingBeatmap beatmap) - : base(score, beatmap) - { - this.score = score; - } - - private FillFlowContainer statisticsContainer; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - const float user_header_height = 120; - - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = user_header_height }, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - } - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new DelayedLoadWrapper(new UserHeader(Score.User) - { - RelativeSizeAxes = Axes.Both, - }) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Height = user_header_height, - }, - new UpdateableRank(Score.Rank) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(150, 60), - Margin = new MarginPadding(20), - }, - scoreContainer = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Height = 60, - Children = new Drawable[] - { - new SongProgressGraph - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.5f, - Objects = Beatmap.Beatmap.HitObjects, - }, - scoreCounter = new SlowScoreCounter(6) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colours.PinkDarker, - Y = 10, - TextSize = 56, - }, - } - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = colours.PinkDarker, - Shadow = false, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Text = "total score", - Margin = new MarginPadding { Bottom = 15 }, - }, - new BeatmapDetails(Beatmap.BeatmapInfo) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Bottom = 10 }, - }, - new DateTimeDisplay(Score.Date.LocalDateTime) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - new Container - { - RelativeSizeAxes = Axes.X, - Size = new Vector2(0.75f, 1), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = 10, Bottom = 10 }, - Children = new Drawable[] - { - new Box - { - Colour = ColourInfo.GradientHorizontal( - colours.GrayC.Opacity(0), - colours.GrayC.Opacity(0.9f)), - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f, 1), - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Colour = ColourInfo.GradientHorizontal( - colours.GrayC.Opacity(0.9f), - colours.GrayC.Opacity(0)), - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f, 1), - }, - } - }, - statisticsContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Direction = FillDirection.Horizontal, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint - }, - }, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Margin = new MarginPadding { Bottom = 10 }, - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new ReplayDownloadButton(score), - new RetryButton() - } - }, - }; - - statisticsContainer.ChildrenEnumerable = Score.SortedStatistics.Select(s => new DrawableScoreStatistic(s)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Schedule(() => - { - scoreCounter.Increment(Score.TotalScore); - - int delay = 0; - - foreach (var s in statisticsContainer.Children) - { - s.FadeOut() - .Then(delay += 200) - .FadeIn(300 + delay, Easing.Out); - } - }); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - scoreCounter.Scale = new Vector2(Math.Min(1f, (scoreContainer.DrawWidth - 20) / scoreCounter.DrawWidth)); - } - - private class DrawableScoreStatistic : Container - { - private readonly KeyValuePair statistic; - - public DrawableScoreStatistic(KeyValuePair statistic) - { - this.statistic = statistic; - - AutoSizeAxes = Axes.Both; - Margin = new MarginPadding { Left = 5, Right = 5 }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Children = new Drawable[] - { - new OsuSpriteText - { - Text = statistic.Value.ToString().PadLeft(4, '0'), - Colour = colours.Gray7, - Font = OsuFont.GetFont(size: 30), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - new OsuSpriteText - { - Text = statistic.Key.GetDescription(), - Colour = colours.Gray7, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Y = 26, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - }; - } - } - - private class DateTimeDisplay : Container - { - private readonly DateTime date; - - public DateTimeDisplay(DateTime date) - { - this.date = date; - - AutoSizeAxes = Axes.Both; - - Masking = true; - CornerRadius = 5; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray6, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Horizontal = 10, Vertical = 5 }, - Spacing = new Vector2(10), - Children = new[] - { - new OsuSpriteText - { - Text = date.ToShortDateString(), - Colour = Color4.White, - }, - new OsuSpriteText - { - Text = date.ToShortTimeString(), - Colour = Color4.White, - } - } - }, - }; - } - } - - private class BeatmapDetails : Container - { - private readonly BeatmapInfo beatmap; - - private readonly OsuSpriteText title; - private readonly OsuSpriteText artist; - private readonly OsuSpriteText versionMapper; - - public BeatmapDetails(BeatmapInfo beatmap) - { - this.beatmap = beatmap; - - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - title = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Shadow = false, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 24, italics: true), - }, - artist = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Shadow = false, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 20, italics: true), - }, - versionMapper = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Shadow = false, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - }, - } - } - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - title.Colour = artist.Colour = colours.BlueDarker; - versionMapper.Colour = colours.Gray8; - - var creator = beatmap.Metadata.Author?.Username; - - if (!string.IsNullOrEmpty(creator)) - { - versionMapper.Text = $"mapped by {creator}"; - - if (!string.IsNullOrEmpty(beatmap.Version)) - versionMapper.Text = $"{beatmap.Version} - " + versionMapper.Text; - } - - title.Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)); - artist.Text = new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); - } - } - - [LongRunningLoad] - private class UserHeader : Container - { - private readonly User user; - private readonly Sprite cover; - - public UserHeader(User user) - { - this.user = user; - Children = new Drawable[] - { - cover = new Sprite - { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = user.Username, - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Regular, italics: true), - Padding = new MarginPadding { Bottom = 10 }, - } - }; - } - - [BackgroundDependencyLoader] - private void load(LargeTextureStore textures) - { - if (!string.IsNullOrEmpty(user.CoverUrl)) - cover.Texture = textures.Get(user.CoverUrl); - } - } - - private class SlowScoreCounter : ScoreCounter - { - protected override double RollingDuration => 3000; - - protected override Easing RollingEasing => Easing.OutPow10; - - public SlowScoreCounter(uint leading = 0) - : base(leading) - { - DisplayedCountSpriteText.Shadow = false; - DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(Typeface.Venera, weight: FontWeight.Light); - UseCommaSeparator = true; - } - } - } -} diff --git a/osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs similarity index 98% rename from osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs rename to osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 62213720aa..a36c86eafc 100644 --- a/osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -9,7 +9,7 @@ using osu.Game.Online; using osu.Game.Scoring; using osuTK; -namespace osu.Game.Screens.Ranking.Pages +namespace osu.Game.Screens.Ranking { public class ReplayDownloadButton : DownloadTrackingComposite { diff --git a/osu.Game/Screens/Ranking/ResultModeButton.cs b/osu.Game/Screens/Ranking/ResultModeButton.cs deleted file mode 100644 index d7eb5db125..0000000000 --- a/osu.Game/Screens/Ranking/ResultModeButton.cs +++ /dev/null @@ -1,97 +0,0 @@ -// 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.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; - -namespace osu.Game.Screens.Ranking -{ - public class ResultModeButton : TabItem, IHasTooltip - { - private readonly IconUsage icon; - private Color4 activeColour; - private Color4 inactiveColour; - private CircularContainer colouredPart; - - public ResultModeButton(IResultPageInfo mode) - : base(mode) - { - icon = mode.Icon; - TooltipText = mode.Name; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Size = new Vector2(50); - - Masking = true; - - CornerRadius = 25; - CornerExponent = 2; - - activeColour = colours.PinkDarker; - inactiveColour = OsuColour.Gray(0.8f); - - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.4f), - Type = EdgeEffectType.Shadow, - Radius = 5, - }; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - colouredPart = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.8f), - BorderThickness = 4, - BorderColour = Color4.White, - Colour = inactiveColour, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, //for border rendering - RelativeSizeAxes = Axes.Both, - Colour = Color4.Transparent, - }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - Colour = OsuColour.Gray(0.95f), - Icon = icon, - Size = new Vector2(20), - } - } - } - }; - } - - protected override void OnActivated() => colouredPart.FadeColour(activeColour, 200, Easing.OutQuint); - - protected override void OnDeactivated() => colouredPart.FadeColour(inactiveColour, 200, Easing.OutQuint); - - public string TooltipText { get; } - } -} diff --git a/osu.Game/Screens/Ranking/ResultModeTabControl.cs b/osu.Game/Screens/Ranking/ResultModeTabControl.cs deleted file mode 100644 index b0d94a4be6..0000000000 --- a/osu.Game/Screens/Ranking/ResultModeTabControl.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.UserInterface; -using osuTK; - -namespace osu.Game.Screens.Ranking -{ - public class ResultModeTabControl : TabControl - { - public ResultModeTabControl() - { - TabContainer.Anchor = Anchor.BottomCentre; - TabContainer.Origin = Anchor.BottomCentre; - TabContainer.Spacing = new Vector2(15); - - TabContainer.Masking = false; - TabContainer.Padding = new MarginPadding(5); - } - - protected override Dropdown CreateDropdown() => null; - - protected override TabItem CreateTabItem(IResultPageInfo value) => new ResultModeButton(value) - { - Anchor = TabContainer.Anchor, - Origin = TabContainer.Origin - }; - } -} diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs deleted file mode 100644 index 05f1872be9..0000000000 --- a/osu.Game/Screens/Ranking/Results.cs +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Screens; -using osu.Game.Graphics.Containers; -using osu.Game.Screens.Backgrounds; -using osuTK; -using osuTK.Graphics; -using osu.Game.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Sprites; -using osu.Game.Scoring; -using osu.Game.Screens.Play; - -namespace osu.Game.Screens.Ranking -{ - public abstract class Results : OsuScreen - { - protected const float BACKGROUND_BLUR = 20; - - private Container circleOuterBackground; - private Container circleOuter; - private Container circleInner; - - private ParallaxContainer backgroundParallax; - - private ResultModeTabControl modeChangeButtons; - - [Resolved(canBeNull: true)] - private Player player { get; set; } - - public override bool DisallowExternalBeatmapRulesetChanges => true; - - protected readonly ScoreInfo Score; - - private Container currentPage; - - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); - - private const float overscan = 1.3f; - - private const float circle_outer_scale = 0.96f; - - protected Results(ScoreInfo score) - { - Score = score; - } - - private const float transition_time = 800; - - private IEnumerable allCircles => new Drawable[] { circleOuterBackground, circleInner, circleOuter }; - - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - ((BackgroundScreenBeatmap)Background).BlurAmount.Value = BACKGROUND_BLUR; - Background.ScaleTo(1.1f, transition_time, Easing.OutQuint); - - allCircles.ForEach(c => - { - c.FadeOut(); - c.ScaleTo(0); - }); - - backgroundParallax.FadeOut(); - modeChangeButtons.FadeOut(); - currentPage?.FadeOut(); - - circleOuterBackground - .FadeIn(transition_time, Easing.OutQuint) - .ScaleTo(1, transition_time, Easing.OutQuint); - - using (BeginDelayedSequence(transition_time * 0.25f, true)) - { - circleOuter - .FadeIn(transition_time, Easing.OutQuint) - .ScaleTo(1, transition_time, Easing.OutQuint); - - using (BeginDelayedSequence(transition_time * 0.3f, true)) - { - backgroundParallax.FadeIn(transition_time, Easing.OutQuint); - - circleInner - .FadeIn(transition_time, Easing.OutQuint) - .ScaleTo(1, transition_time, Easing.OutQuint); - - using (BeginDelayedSequence(transition_time * 0.4f, true)) - { - modeChangeButtons.FadeIn(transition_time, Easing.OutQuint); - currentPage?.FadeIn(transition_time, Easing.OutQuint); - } - } - } - } - - public override bool OnExiting(IScreen next) - { - allCircles.ForEach(c => c.ScaleTo(0, transition_time, Easing.OutSine)); - - Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint); - - this.FadeOut(transition_time / 4); - - return base.OnExiting(next); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - InternalChild = new AspectContainer - { - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Height = overscan, - Children = new Drawable[] - { - circleOuterBackground = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new Box - { - Alpha = 0.2f, - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - } - } - }, - circleOuter = new CircularContainer - { - Size = new Vector2(circle_outer_scale), - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.4f), - Type = EdgeEffectType.Shadow, - Radius = 15, - }, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - backgroundParallax = new ParallaxContainer - { - RelativeSizeAxes = Axes.Both, - ParallaxAmount = 0.01f, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Sprite - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.2f, - Texture = Beatmap.Value.Background, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill - } - } - }, - modeChangeButtons = new ResultModeTabControl - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 50, - Margin = new MarginPadding { Bottom = 110 }, - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomCentre, - Text = $"{Score.MaxCombo}x", - RelativePositionAxes = Axes.X, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40), - X = 0.1f, - Colour = colours.BlueDarker, - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopCentre, - Text = "max combo", - Font = OsuFont.GetFont(size: 20), - RelativePositionAxes = Axes.X, - X = 0.1f, - Colour = colours.Gray6, - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomCentre, - Text = Score.DisplayAccuracy, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40), - RelativePositionAxes = Axes.X, - X = 0.9f, - Colour = colours.BlueDarker, - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopCentre, - Text = "accuracy", - Font = OsuFont.GetFont(size: 20), - RelativePositionAxes = Axes.X, - X = 0.9f, - Colour = colours.Gray6, - }, - } - }, - circleInner = new CircularContainer - { - Size = new Vector2(0.6f), - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.4f), - Type = EdgeEffectType.Shadow, - Radius = 15, - }, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - } - } - } - }; - - if (player != null) - { - AddInternal(new HotkeyRetryOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - player?.Restart(); - }, - }); - } - - var pages = CreateResultPages(); - - foreach (var p in pages) - modeChangeButtons.AddItem(p); - - modeChangeButtons.Current.Value = pages.FirstOrDefault(); - - modeChangeButtons.Current.BindValueChanged(page => - { - currentPage?.FadeOut(); - currentPage?.Expire(); - - currentPage = page.NewValue?.CreatePage(); - - if (currentPage != null) - LoadComponentAsync(currentPage, circleInner.Add); - }, true); - } - - protected abstract IEnumerable CreateResultPages(); - } -} diff --git a/osu.Game/Screens/Ranking/ResultsPage.cs b/osu.Game/Screens/Ranking/ResultsPage.cs deleted file mode 100644 index 8776c599dd..0000000000 --- a/osu.Game/Screens/Ranking/ResultsPage.cs +++ /dev/null @@ -1,92 +0,0 @@ -// 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.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Scoring; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Ranking -{ - public abstract class ResultsPage : Container - { - protected readonly ScoreInfo Score; - protected readonly WorkingBeatmap Beatmap; - private CircularContainer content; - private Box fill; - - protected override Container Content => content; - - protected ResultsPage(ScoreInfo score, WorkingBeatmap beatmap) - { - Score = score; - Beatmap = beatmap; - RelativeSizeAxes = Axes.Both; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - fill.Delay(400).FadeInFromZero(600); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - AddRangeInternal(new Drawable[] - { - fill = new Box - { - Alpha = 0, - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray6 - }, - new CircularContainer - { - EdgeEffect = new EdgeEffectParameters - { - Colour = colours.GrayF.Opacity(0.8f), - Type = EdgeEffectType.Shadow, - Radius = 1, - }, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = 20, - BorderColour = Color4.White, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - } - }, - content = new CircularContainer - { - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.2f), - Type = EdgeEffectType.Shadow, - Radius = 15, - }, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.88f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }); - } - } -} diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs new file mode 100644 index 0000000000..d0e02329fe --- /dev/null +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -0,0 +1,97 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; +using osu.Game.Scoring; +using osu.Game.Screens.Backgrounds; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public class ResultsScreen : OsuScreen + { + protected const float BACKGROUND_BLUR = 20; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + // Temporary for now to stop dual transitions. Should respect the current toolbar mode, but there's no way to do so currently. + public override bool HideOverlaysOnEnter => true; + + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); + + private readonly ScoreInfo score; + + private Drawable bottomPanel; + + public ResultsScreen(ScoreInfo score) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new[] + { + new ScorePanel(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = PanelState.Expanded + }, + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ReplayDownloadButton(score), + new RetryButton() + } + } + } + } + }; + } + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + ((BackgroundScreenBeatmap)Background).BlurAmount.Value = BACKGROUND_BLUR; + + Background.FadeTo(0.5f, 250); + bottomPanel.FadeTo(1, 250); + } + + public override bool OnExiting(IScreen next) + { + Background.FadeTo(1, 250); + + return base.OnExiting(next); + } + } +} diff --git a/osu.Game/Screens/Ranking/Pages/RetryButton.cs b/osu.Game/Screens/Ranking/RetryButton.cs similarity index 97% rename from osu.Game/Screens/Ranking/Pages/RetryButton.cs rename to osu.Game/Screens/Ranking/RetryButton.cs index 06d0440b30..59b69bc949 100644 --- a/osu.Game/Screens/Ranking/Pages/RetryButton.cs +++ b/osu.Game/Screens/Ranking/RetryButton.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play; using osuTK; -namespace osu.Game.Screens.Ranking.Pages +namespace osu.Game.Screens.Ranking { public class RetryButton : OsuAnimatedButton { diff --git a/osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs b/osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs deleted file mode 100644 index fe183c5f89..0000000000 --- a/osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking.Pages; - -namespace osu.Game.Screens.Ranking.Types -{ - public class LocalLeaderboardPageInfo : IResultPageInfo - { - private readonly ScoreInfo score; - private readonly WorkingBeatmap beatmap; - - public LocalLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap) - { - this.score = score; - this.beatmap = beatmap; - } - - public IconUsage Icon => FontAwesome.Solid.User; - - public string Name => @"Local Leaderboard"; - - public ResultsPage CreatePage() => new LocalLeaderboardPage(score, beatmap); - } -} diff --git a/osu.Game/Screens/Ranking/Types/ScoreOverviewPageInfo.cs b/osu.Game/Screens/Ranking/Types/ScoreOverviewPageInfo.cs deleted file mode 100644 index 424dbff6f6..0000000000 --- a/osu.Game/Screens/Ranking/Types/ScoreOverviewPageInfo.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking.Pages; - -namespace osu.Game.Screens.Ranking.Types -{ - public class ScoreOverviewPageInfo : IResultPageInfo - { - public IconUsage Icon => FontAwesome.Solid.Asterisk; - - public string Name => "Overview"; - private readonly ScoreInfo score; - private readonly WorkingBeatmap beatmap; - - public ScoreOverviewPageInfo(ScoreInfo score, WorkingBeatmap beatmap) - { - this.score = score; - this.beatmap = beatmap; - } - - public ResultsPage CreatePage() - { - return new ScoreResultsPage(score, beatmap); - } - } -} diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index af113781e5..9e2f5761dd 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -8,6 +8,7 @@ using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osu.Game.Users; using osuTK.Input; @@ -31,7 +32,7 @@ namespace osu.Game.Screens.Select Edit(); }, Key.Number4); - ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score)); + ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new ResultsScreen(score)); } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); From 86a336d58537c8c3abbe550ea260a127703f3863 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:45:25 +0900 Subject: [PATCH 313/387] Add back retry overlay --- osu.Game/Screens/Ranking/ResultsScreen.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index d0e02329fe..89547fc5dc 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Screens.Ranking @@ -25,6 +26,9 @@ namespace osu.Game.Screens.Ranking protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); + [Resolved(CanBeNull = true)] + private Player player { get; set; } + private readonly ScoreInfo score; private Drawable bottomPanel; @@ -75,6 +79,19 @@ namespace osu.Game.Screens.Ranking } } }; + + if (player != null) + { + AddInternal(new HotkeyRetryOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + player?.Restart(); + }, + }); + } } public override void OnEntering(IScreen last) From 6f569d148515b6276c51fabd742bffaa6c7b7cd0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 18:01:46 +0900 Subject: [PATCH 314/387] Fix colour conflicts for expert-plus --- .../Ranking/TestSceneStarRatingDisplay.cs | 32 +++++++++++++++++++ .../Ranking/Expanded/StarRatingDisplay.cs | 8 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs new file mode 100644 index 0000000000..d12f32e470 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs @@ -0,0 +1,32 @@ +// 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.Game.Beatmaps; +using osu.Game.Screens.Ranking.Expanded; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneStarRatingDisplay : OsuTestScene + { + public TestSceneStarRatingDisplay() + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 1.23 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 2.34 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 3.45 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 4.56 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 5.67 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 6.78 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 10.11 }), + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs index 74b58b9f8c..4b38b298f1 100644 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -3,7 +3,9 @@ using System.Globalization; 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.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; @@ -40,6 +42,10 @@ namespace osu.Game.Screens.Ranking.Expanded string fractionPart = starRatingParts[1]; string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; + ColourInfo backgroundColour = beatmap.DifficultyRating == DifficultyRating.ExpertPlus + ? ColourInfo.GradientVertical(Color4Extensions.FromHex("#C1C1C1"), Color4Extensions.FromHex("#595959")) + : (ColourInfo)colours.ForDifficultyRating(beatmap.DifficultyRating); + InternalChildren = new Drawable[] { new CircularContainer @@ -51,7 +57,7 @@ namespace osu.Game.Screens.Ranking.Expanded new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.ForDifficultyRating(beatmap.DifficultyRating) + Colour = backgroundColour }, } }, From f06c170d63d5d7b69ed2717eac3fb4e08be787ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 18:04:15 +0900 Subject: [PATCH 315/387] Display SS badge earlier (when entering virtual ss area) --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 873c20cc2b..4b6f777283 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (badge.Accuracy > score.Accuracy) continue; - using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, badge.Accuracy / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) + using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(1 - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) badge.Appear(); } From df119eb95a8177093be99e7c039053aa50cd56ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 18:04:32 +0900 Subject: [PATCH 316/387] Adjust animations --- .../Ranking/Expanded/Accuracy/RankText.cs | 66 +++++++++++++++++-- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs index b803fe6022..8343716e7e 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs @@ -4,11 +4,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Accuracy { @@ -19,7 +21,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { private readonly ScoreRank rank; - private Drawable flash; + private BufferedContainer flash; + private BufferedContainer superFlash; + private GlowingSpriteText rankText; public RankText(ScoreRank rank) { @@ -35,17 +39,38 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy [BackgroundDependencyLoader] private void load() { - InternalChildren = new[] + InternalChildren = new Drawable[] { - new GlowingSpriteText + rankText = new GlowingSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, + GlowColour = OsuColour.ForRank(rank), Spacing = new Vector2(-15, 0), Text = DrawableRank.GetRankName(rank), Font = OsuFont.Numeric.With(size: 76), UseFullGlyphHeight = false }, + superFlash = new BufferedContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BlurSigma = new Vector2(85), + Size = new Vector2(600), + CacheDrawnFrameBuffer = true, + Blending = BlendingParameters.Additive, + Alpha = 0, + Children = new[] + { + new Box + { + Colour = Color4.White, + Size = new Vector2(150), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }, + }, flash = new BufferedContainer { Anchor = Anchor.Centre, @@ -53,8 +78,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy BlurSigma = new Vector2(35), BypassAutoSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both, + CacheDrawnFrameBuffer = true, Blending = BlendingParameters.Additive, - Size = new Vector2(2f), + Alpha = 0, + Size = new Vector2(2f), // increase buffer size to allow for scale Scale = new Vector2(1.8f), Children = new[] { @@ -75,9 +102,36 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy public void Appear() { - this.FadeIn(0, Easing.In); + this.FadeIn(); - flash.FadeIn(0, Easing.In).Then().FadeOut(800, Easing.OutQuint); + if (rank < ScoreRank.A) + { + this + .MoveToOffset(new Vector2(0, -20)) + .MoveToOffset(new Vector2(0, 20), 200, Easing.OutBounce); + + if (rank <= ScoreRank.D) + { + this.Delay(700) + .RotateTo(5, 150, Easing.In) + .MoveToOffset(new Vector2(0, 3), 150, Easing.In); + } + + this.FadeInFromZero(200, Easing.OutQuint); + return; + } + + flash.Colour = OsuColour.ForRank(rank); + flash.FadeIn().Then().FadeOut(1200, Easing.OutQuint); + + if (rank >= ScoreRank.S) + rankText.ScaleTo(1.05f).ScaleTo(1, 3000, Easing.OutQuint); + + if (rank >= ScoreRank.X) + { + flash.FadeIn().Then().FadeOut(3000); + superFlash.FadeIn().Then().FadeOut(800, Easing.OutQuint); + } } } } From 370ff70dd441aa1d02d82b872bb339eb468048e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 18:32:30 +0900 Subject: [PATCH 317/387] Fix incorrect host name specification --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index c1bd73ef05..c6095ae404 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -417,7 +417,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithDuplicateHashes() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateHashes))) { try { From 48cbec7a319383a892d1dc12d99c8d33a5748d3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 22:21:16 +0900 Subject: [PATCH 318/387] Add scroll view because --- osu.Game/Screens/Ranking/ResultsScreen.cs | 36 ++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 89547fc5dc..6f8b5d19df 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; @@ -43,11 +44,14 @@ namespace osu.Game.Screens.Ranking { InternalChildren = new[] { - new ScorePanel(score) + new ResultsScrollContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = PanelState.Expanded + Child = new ScorePanel(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = PanelState.Expanded + }, }, bottomPanel = new Container { @@ -110,5 +114,29 @@ namespace osu.Game.Screens.Ranking return base.OnExiting(next); } + + private class ResultsScrollContainer : OsuScrollContainer + { + private readonly Container content; + + protected override Container Content => content; + + public ResultsScrollContainer() + { + base.Content.Add(content = new Container + { + RelativeSizeAxes = Axes.X + }); + + RelativeSizeAxes = Axes.Both; + ScrollbarVisible = false; + } + + protected override void Update() + { + base.Update(); + content.Height = DrawHeight; + } + } } } From 24b944fc8e4ba12346052376f7d5ff982544f31d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 22:24:28 +0900 Subject: [PATCH 319/387] Make footer buttons wider --- osu.Game/Screens/Ranking/ResultsScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 6f8b5d19df..0952ba1f70 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -76,8 +76,8 @@ namespace osu.Game.Screens.Ranking Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ReplayDownloadButton(score), - new RetryButton() + new ReplayDownloadButton(score) { Width = 300 }, + new RetryButton { Width = 300 }, } } } From 27cc68152d426af6025321414361423437321f91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 22:54:02 +0900 Subject: [PATCH 320/387] Fix potentially missing metadata on local score display --- .../Expanded/ExpandedPanelMiddleContent.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 6d5d7e0d95..cc376d7dcc 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -45,8 +46,16 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load() + private void load(BeatmapManager beatmaps) { + var metadata = score.Beatmap.Metadata; + + if (metadata == null) + { + var beatmap = beatmaps.QueryBeatmap(b => b.ID == score.BeatmapInfoID); + metadata = beatmap.Metadata ?? beatmap.BeatmapSet.Metadata; + } + var topStatistics = new List { new AccuracyStatistic(score.Accuracy), @@ -81,14 +90,14 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((score.Beatmap.Metadata.Title, score.Beatmap.Metadata.TitleUnicode)), + Text = new LocalisedString((metadata.Title, metadata.TitleUnicode)), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((score.Beatmap.Metadata.Artist, score.Beatmap.Metadata.ArtistUnicode)), + Text = new LocalisedString((metadata.Artist, metadata.ArtistUnicode)), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold) }, new Container From dd3a6c5673fc25ede3838a56c03399a5ceeb0dfe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 23:13:31 +0900 Subject: [PATCH 321/387] Use working beatmap to retrieve metadata for now --- .../Expanded/ExpandedPanelMiddleContent.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index cc376d7dcc..6b7e4c9cb4 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -46,15 +47,10 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps) + private void load(Bindable working) { - var metadata = score.Beatmap.Metadata; - - if (metadata == null) - { - var beatmap = beatmaps.QueryBeatmap(b => b.ID == score.BeatmapInfoID); - metadata = beatmap.Metadata ?? beatmap.BeatmapSet.Metadata; - } + var beatmap = working.Value.BeatmapInfo; + var metadata = beatmap.Metadata; var topStatistics = new List { @@ -129,7 +125,7 @@ namespace osu.Game.Screens.Ranking.Expanded AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new StarRatingDisplay(score.Beatmap) + new StarRatingDisplay(beatmap) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft @@ -157,7 +153,7 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = score.Beatmap.Version, + Text = beatmap.Version, Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), }, new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) From 592d8cbd1343d4a38c64c0e47fa5b12edf4b4cb8 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Tue, 17 Mar 2020 22:45:28 +0700 Subject: [PATCH 322/387] Fix mapper name in score panel --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 6b7e4c9cb4..837467d648 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Ranking.Expanded }.With(t => { t.AddText("mapped by "); - t.AddText(score.UserString, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + t.AddText(score.Beatmap.Metadata.Author.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); }) } }, From 99409fb7d1c6596c0aa5d4e8435907b4d92c9b19 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Tue, 17 Mar 2020 23:02:39 +0700 Subject: [PATCH 323/387] Fix mapper info alignment in score panel --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 6b7e4c9cb4..d542a6f033 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -158,6 +158,8 @@ namespace osu.Game.Screens.Ranking.Expanded }, new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, }.With(t => From d18b21ba329f5fe7988605791cf6c01a99ab3ead Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Tue, 17 Mar 2020 23:23:51 +0700 Subject: [PATCH 324/387] Use local variable for metadata instead --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 837467d648..f92c8df012 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Ranking.Expanded }.With(t => { t.AddText("mapped by "); - t.AddText(score.Beatmap.Metadata.Author.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + t.AddText(metadata.Author.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); }) } }, From 431571dfa07c8c1b5cb6a932ecbdcf5874f6a874 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 18 Mar 2020 00:15:43 +0700 Subject: [PATCH 325/387] Check nulls --- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index f92c8df012..eb5da19303 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -51,6 +51,7 @@ namespace osu.Game.Screens.Ranking.Expanded { var beatmap = working.Value.BeatmapInfo; var metadata = beatmap.Metadata; + var creator = beatmap.Metadata.Author?.Username; var topStatistics = new List { @@ -162,8 +163,11 @@ namespace osu.Game.Screens.Ranking.Expanded Direction = FillDirection.Horizontal, }.With(t => { - t.AddText("mapped by "); - t.AddText(metadata.Author.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + if (!string.IsNullOrEmpty(creator)) + { + t.AddText("mapped by "); + t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + } }) } }, From 100c9d422fbd043943e9743f57a3aeb00f2b23bd Mon Sep 17 00:00:00 2001 From: Tina Date: Tue, 17 Mar 2020 18:55:30 +0100 Subject: [PATCH 326/387] Re-use colors defined for each rank in result screen accuracy circle --- .../Ranking/Expanded/Accuracy/AccuracyCircle.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 4b6f777283..2d76a7c3b0 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -119,42 +119,42 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#BE0089"), + Colour = OsuColour.ForRank(ScoreRank.X), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 1 } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#0096A2"), + Colour = OsuColour.ForRank(ScoreRank.S), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 1 - virtual_ss_percentage } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#72C904"), + Colour = OsuColour.ForRank(ScoreRank.A), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.95f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#D99D03"), + Colour = OsuColour.ForRank(ScoreRank.B), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.9f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#EA7948"), + Colour = OsuColour.ForRank(ScoreRank.C), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.8f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#FF5858"), + Colour = OsuColour.ForRank(ScoreRank.D), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.7f } }, From 139ae2bc1e3c65157ba228b69b459f688a102ae8 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 18 Mar 2020 01:24:58 +0700 Subject: [PATCH 327/387] Use existing variables instead --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index eb5da19303..82a2bd87ec 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Ranking.Expanded { var beatmap = working.Value.BeatmapInfo; var metadata = beatmap.Metadata; - var creator = beatmap.Metadata.Author?.Username; + var creator = metadata.Author?.Username; var topStatistics = new List { From dc73105a106473d4a98c854cdea4cd93eb72e112 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 18 Mar 2020 01:33:01 +0700 Subject: [PATCH 328/387] Add tests for beatmaps with(out) null mappers --- .../TestSceneExpandedPanelMiddleContent.cs | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 665b3ad455..fb8c438fa4 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -3,10 +3,13 @@ using System; using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -23,6 +26,11 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneExpandedPanelMiddleContent : OsuTestScene { + [Resolved] + private BeatmapManager beatmaps { get; set; } + + private User author; + public override IReadOnlyList RequiredTypes => new[] { typeof(ExpandedPanelMiddleContent), @@ -35,8 +43,37 @@ namespace osu.Game.Tests.Visual.Ranking typeof(TotalScoreCounter) }; - public TestSceneExpandedPanelMiddleContent() + protected override void LoadComplete() { + base.LoadComplete(); + + var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); + if (beatmapInfo != null) + { + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); + author = Beatmap.Value.Metadata.Author; + } + } + + [Test] + public void TestExampleScore() + { + addScoreStep(createTestScore()); + } + + [Test] + public void TestScoreWithNullAuthor() + { + AddStep("set author to null", () => { + Beatmap.Value.Metadata.Author = null; + }); + addScoreStep(createTestScore()); + AddStep("set author to not null", () => { + Beatmap.Value.Metadata.Author = author; + }); + } + + private void addScoreStep(ScoreInfo score) => AddStep("add panel", () => { Child = new Container { Anchor = Anchor.Centre, @@ -49,10 +86,10 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#444"), }, - new ExpandedPanelMiddleContent(createTestScore()) + new ExpandedPanelMiddleContent(score) } }; - } + }); private ScoreInfo createTestScore() => new ScoreInfo { From 7186e3466bd36dce2e1433bab1f3666ad5d9f6a8 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 18 Mar 2020 01:39:19 +0700 Subject: [PATCH 329/387] Fix formatting issues --- .../Ranking/TestSceneExpandedPanelMiddleContent.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index fb8c438fa4..7a20ba6fd0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -48,6 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking base.LoadComplete(); var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); + if (beatmapInfo != null) { Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); @@ -64,16 +65,19 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestScoreWithNullAuthor() { - AddStep("set author to null", () => { + AddStep("set author to null", () => + { Beatmap.Value.Metadata.Author = null; }); addScoreStep(createTestScore()); - AddStep("set author to not null", () => { + AddStep("set author to not null", () => + { Beatmap.Value.Metadata.Author = author; }); } - private void addScoreStep(ScoreInfo score) => AddStep("add panel", () => { + private void addScoreStep(ScoreInfo score) => AddStep("add panel", () => + { Child = new Container { Anchor = Anchor.Centre, From e951979a12ec05aadce4e4c0b68435d2a24f9f75 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 22:34:46 +0300 Subject: [PATCH 330/387] Remove assert from online test --- .../Visual/Online/TestSceneFriendsLayout.cs | 18 +++++------------- .../Dashboard/Friends/FriendsLayout.cs | 10 +++++----- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index c6971a971d..e6b2a41c1c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -10,7 +10,6 @@ using osu.Game.Users; using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; -using System.Linq; using osu.Game.Online.API; namespace osu.Game.Tests.Visual.Online @@ -32,7 +31,7 @@ namespace osu.Game.Tests.Visual.Online [Resolved] private IAPIProvider api { get; set; } - private TestFriendsLayout layout; + private FriendsLayout layout; [SetUp] public void Setup() => Schedule(() => @@ -40,21 +39,19 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = layout = new TestFriendsLayout() + Child = layout = new FriendsLayout() }; }); [Test] - public void TestOnline() + public void TestOffline() { - // Skip online test if user is not logged-in - AddUntilStep("Users loaded", () => !api.IsLoggedIn || (layout?.StatusControl.Items.Any() ?? false)); + AddStep("Populate", () => layout.Users = getUsers()); } [Test] - public void TestPopulate() + public void TestOnline() { - AddStep("Populate", () => layout.Users = getUsers()); } private List getUsers() => new List @@ -89,10 +86,5 @@ namespace osu.Game.Tests.Visual.Online LastVisit = DateTimeOffset.Now } }; - - private class TestFriendsLayout : FriendsLayout - { - public FriendsOnlineStatusControl StatusControl => OnlineStatusControl; - } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index c02f07fe4a..55a5081435 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Dashboard.Friends users = value; - OnlineStatusControl.Populate(value); + onlineStatusControl.Populate(value); } } @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Dashboard.Friends private Drawable currentContent; - protected readonly FriendsOnlineStatusControl OnlineStatusControl; + private readonly FriendsOnlineStatusControl onlineStatusControl; private readonly Box background; private readonly Box controlBackground; private readonly UserListToolbar userListToolbar; @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Top = 20, Horizontal = 45 }, - Child = OnlineStatusControl = new FriendsOnlineStatusControl(), + Child = onlineStatusControl = new FriendsOnlineStatusControl(), } } }, @@ -149,7 +149,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { base.LoadComplete(); - OnlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); + onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); @@ -171,7 +171,7 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var groupedUsers = OnlineStatusControl.Current.Value?.Users ?? new List(); + var groupedUsers = onlineStatusControl.Current.Value?.Users ?? new List(); var sortedUsers = sortUsers(groupedUsers); From 944f0b0285e51e32f9588d699d1f08c4fbda4f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Mar 2020 20:45:48 +0100 Subject: [PATCH 331/387] Rewrite tests * Use [Cached] injection instead of modifying beatmaps read from store. * Add assertion steps verifying the presence of mapper name (or lack thereof). --- .../TestSceneExpandedPanelMiddleContent.cs | 91 ++++++++++--------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 7a20ba6fd0..52d8ea0480 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -3,13 +3,18 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -27,9 +32,7 @@ namespace osu.Game.Tests.Visual.Ranking public class TestSceneExpandedPanelMiddleContent : OsuTestScene { [Resolved] - private BeatmapManager beatmaps { get; set; } - - private User author; + private RulesetStore rulesetStore { get; set; } public override IReadOnlyList RequiredTypes => new[] { @@ -43,57 +46,37 @@ namespace osu.Game.Tests.Visual.Ranking typeof(TotalScoreCounter) }; - protected override void LoadComplete() + [Test] + public void TestMapWithKnownMapper() { - base.LoadComplete(); + var author = new User { Username = "mapper_name" }; - var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); + AddStep("show example score", () => showPanel(createTestBeatmap(author), createTestScore())); - if (beatmapInfo != null) - { - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); - author = Beatmap.Value.Metadata.Author; - } + AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); } [Test] - public void TestExampleScore() + public void TestMapWithUnknownMapper() { - addScoreStep(createTestScore()); + AddStep("show example score", () => showPanel(createTestBeatmap(null), createTestScore())); + + AddAssert("mapped by text not present", () => + this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); } - [Test] - public void TestScoreWithNullAuthor() + private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) { - AddStep("set author to null", () => - { - Beatmap.Value.Metadata.Author = null; - }); - addScoreStep(createTestScore()); - AddStep("set author to not null", () => - { - Beatmap.Value.Metadata.Author = author; - }); + Child = new ExpandedPanelMiddleContentContainer(workingBeatmap, score); } - private void addScoreStep(ScoreInfo score) => AddStep("add panel", () => + private WorkingBeatmap createTestBeatmap(User author) { - Child = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(500, 700), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#444"), - }, - new ExpandedPanelMiddleContent(score) - } - }; - }); + var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)); + beatmap.Metadata.Author = author; + + return new TestWorkingBeatmap(beatmap); + } private ScoreInfo createTestScore() => new ScoreInfo { @@ -117,5 +100,31 @@ namespace osu.Game.Tests.Visual.Ranking { HitResult.Great, 300 }, } }; + + private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); + + private class ExpandedPanelMiddleContentContainer : Container + { + [Cached] + private Bindable workingBeatmap { get; set; } + + public ExpandedPanelMiddleContentContainer(WorkingBeatmap beatmap, ScoreInfo score) + { + workingBeatmap = new Bindable(beatmap); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Size = new Vector2(500, 700); + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#444"), + }, + new ExpandedPanelMiddleContent(score) + }; + } + } } } From bee8e22d185e39abca1e54886fc3bb3998f3b8a4 Mon Sep 17 00:00:00 2001 From: Fuewburvpoa Date: Tue, 17 Mar 2020 22:27:11 +0200 Subject: [PATCH 332/387] Fix BeatDivisorControl allow to select value outside of VALID_DIVISORS --- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 8201ec2710..626a12edcf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -279,6 +279,10 @@ namespace osu.Game.Screens.Edit.Compose.Components handleMouseInput(e.ScreenSpaceMousePosition); } + protected override void OnDragEnd(DragEndEvent e) + { + } + private void handleMouseInput(Vector2 screenSpaceMousePosition) { // copied from SliderBar so we can do custom spacing logic. From 6c825eb74499799fe5dd7c07f56fbd0359862c9f Mon Sep 17 00:00:00 2001 From: Fuewburvpoa <44163588+Fuewburvpoa@users.noreply.github.com> Date: Wed, 18 Mar 2020 00:04:03 +0200 Subject: [PATCH 333/387] Update osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 626a12edcf..2dec3fd22e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -281,6 +281,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnDragEnd(DragEndEvent e) { + handleMouseInput(e.ScreenSpaceMousePosition); } private void handleMouseInput(Vector2 screenSpaceMousePosition) From a5c0da392edd913928371f4e2e7ea072518cb80a Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Tue, 17 Mar 2020 18:22:29 -0400 Subject: [PATCH 334/387] fix 'good first issue' link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77c7eb9d2d..b44a529f2b 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted. -If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) label). +If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/labels/good-first-issue) label). Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**. From 23e698c8a5d025221dc6fad9d183b2633ef14840 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Tue, 17 Mar 2020 18:26:41 -0400 Subject: [PATCH 335/387] sort by most recently updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b44a529f2b..59d72247f5 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted. -If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/labels/good-first-issue) label). +If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aopen+label%3Agood-first-issue+sort%3Aupdated-desc) label). Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**. From 3c07f73c7b1b559e848419d3af6fc0b047db56ec Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 17 Mar 2020 17:32:58 -0700 Subject: [PATCH 336/387] Fix results' beatmap title and artist language setting being swapped --- .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 82a2bd87ec..d64008e6db 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -87,14 +87,14 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((metadata.Title, metadata.TitleUnicode)), + Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((metadata.Artist, metadata.ArtistUnicode)), + Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold) }, new Container From 044707adb7d1f1d17625e46bcfd242f728b3b699 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Mar 2020 12:10:51 +0900 Subject: [PATCH 337/387] Update incorrect file path causing error on rider solution load --- .idea/.idea.osu.Desktop/.idea/modules.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml index fe63f5faf3..366f172c30 100644 --- a/.idea/.idea.osu.Desktop/.idea/modules.xml +++ b/.idea/.idea.osu.Desktop/.idea/modules.xml @@ -2,6 +2,7 @@ + From 44cfed8af1cb7a577f67c5c25c895eb2fb6c2a51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Mar 2020 15:03:01 +0900 Subject: [PATCH 338/387] Fix perfect display showing when misses are present --- .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 7a50a96fe7..cd2534bd31 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking.Expanded.Accuracy; @@ -56,7 +57,7 @@ namespace osu.Game.Screens.Ranking.Expanded var topStatistics = new List { new AccuracyStatistic(score.Accuracy), - new ComboStatistic(score.MaxCombo, true), + new ComboStatistic(score.MaxCombo, score.Statistics[HitResult.Miss] == 0), new CounterStatistic("pp", (int)(score.PP ?? 0)), }; From 336d92715755320e33ab6c2f17095935a42b99c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Mar 2020 15:38:19 +0900 Subject: [PATCH 339/387] Update tests to not use positional data (nunit runs at an incompatible window size) --- .../Screens/TestSceneMapPoolScreen.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index cc5f66761e..a4538be384 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -38,7 +39,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + assertTwoWide(); } [Test] @@ -58,7 +59,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + assertTwoWide(); } [Test] @@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); + assertThreeWide(); } [Test] @@ -98,9 +99,15 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + assertTwoWide(); } + private void assertTwoWide() => + AddAssert("ensure layout width is 2", () => screen.ChildrenOfType>>().First().Padding.Left > 0); + + private void assertThreeWide() => + AddAssert("ensure layout width is 3", () => screen.ChildrenOfType>>().First().Padding.Left == 0); + [Test] public void TestManyMods() { @@ -118,7 +125,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); + assertThreeWide(); } private void addBeatmap(string mods = "nm") From fdcb60706b286ea8c4c8479206642d4fe8ad9a7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Mar 2020 15:49:24 +0900 Subject: [PATCH 340/387] Use TryGetValue to make tests happy --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index cd2534bd31..4ae9bb05cc 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Ranking.Expanded var topStatistics = new List { new AccuracyStatistic(score.Accuracy), - new ComboStatistic(score.MaxCombo, score.Statistics[HitResult.Miss] == 0), + new ComboStatistic(score.MaxCombo, !score.Statistics.TryGetValue(HitResult.Miss, out var missCount) || missCount == 0), new CounterStatistic("pp", (int)(score.PP ?? 0)), }; From 5f09c70f756f6548577a8a28d21335a7ce4df43f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:21:36 +0900 Subject: [PATCH 341/387] Move judgement colours to OsuColour --- osu.Game/Graphics/OsuColour.cs | 27 +++++++++++++++++++ .../Rulesets/Judgements/DrawableJudgement.cs | 26 +----------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index f7ed55410c..caf09a7df6 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -3,6 +3,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osuTK.Graphics; @@ -67,6 +68,32 @@ namespace osu.Game.Graphics } } + /// + /// Retrieves the colour for a . + /// + public Color4 ForHitResult(HitResult judgement) + { + switch (judgement) + { + case HitResult.Perfect: + case HitResult.Great: + return Blue; + + case HitResult.Ok: + case HitResult.Good: + return Green; + + case HitResult.Meh: + return Yellow; + + case HitResult.Miss: + return Red; + + default: + return Color4.White; + } + } + // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 960585b968..7113acbbfb 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; -using osuTK.Graphics; namespace osu.Game.Rulesets.Judgements { @@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Judgements { Text = Result.Type.GetDescription().ToUpperInvariant(), Font = OsuFont.Numeric.With(size: 20), - Colour = judgementColour(Result.Type), + Colour = colours.ForHitResult(Result.Type), Scale = new Vector2(0.85f, 1), }, confineMode: ConfineMode.NoScaling) }; @@ -110,28 +109,5 @@ namespace osu.Game.Rulesets.Judgements Expire(true); } - - private Color4 judgementColour(HitResult judgement) - { - switch (judgement) - { - case HitResult.Perfect: - case HitResult.Great: - return colours.Blue; - - case HitResult.Ok: - case HitResult.Good: - return colours.Green; - - case HitResult.Meh: - return colours.Yellow; - - case HitResult.Miss: - return colours.Red; - - default: - return Color4.White; - } - } } } From 66558ca8c527e44a5739389cf7dd0a716fe65b1b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:21:57 +0900 Subject: [PATCH 342/387] Colourise hit result statistics --- .../Expanded/ExpandedPanelMiddleContent.cs | 3 +-- .../Expanded/Statistics/HitResultStatistic.cs | 27 +++++++++++++++++++ .../Expanded/Statistics/StatisticDisplay.cs | 6 +++-- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 7a50a96fe7..1de071a228 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; @@ -62,7 +61,7 @@ namespace osu.Game.Screens.Ranking.Expanded var bottomStatistics = new List(); foreach (var stat in score.SortedStatistics) - bottomStatistics.Add(new CounterStatistic(stat.Key.GetDescription(), stat.Value)); + bottomStatistics.Add(new HitResultStatistic(stat.Key, stat.Value)); statisticDisplays.AddRange(topStatistics); statisticDisplays.AddRange(bottomStatistics); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs new file mode 100644 index 0000000000..faa4a6a96c --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs @@ -0,0 +1,27 @@ +// 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.Extensions; +using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class HitResultStatistic : CounterStatistic + { + private readonly HitResult result; + + public HitResultStatistic(HitResult result, int count) + : base(result.GetDescription(), count) + { + this.result = result; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + HeaderText.Colour = colours.ForHitResult(result); + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs index a653cc82d4..9206c58bc9 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs @@ -6,6 +6,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -16,8 +17,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics ///
public abstract class StatisticDisplay : CompositeDrawable { - private readonly string header; + protected SpriteText HeaderText { get; private set; } + private readonly string header; private Drawable content; /// @@ -53,7 +55,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#222") }, - new OsuSpriteText + HeaderText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, From b91dc15dbf2b39fb4f8c5c51bafaaec4d8bc8cb0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:37:03 +0900 Subject: [PATCH 343/387] Update rank badge colours --- osu.Game/Graphics/OsuColour.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index f7ed55410c..aa5c424599 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -47,23 +47,23 @@ namespace osu.Game.Graphics { case ScoreRank.XH: case ScoreRank.X: - return Color4Extensions.FromHex(@"ce1c9d"); + return Color4Extensions.FromHex(@"de31ae"); case ScoreRank.SH: case ScoreRank.S: - return Color4Extensions.FromHex(@"00a8b5"); + return Color4Extensions.FromHex(@"02b5c3"); case ScoreRank.A: - return Color4Extensions.FromHex(@"7cce14"); + return Color4Extensions.FromHex(@"88da20"); case ScoreRank.B: return Color4Extensions.FromHex(@"e3b130"); case ScoreRank.C: - return Color4Extensions.FromHex(@"f18252"); + return Color4Extensions.FromHex(@"ff8e5d"); default: - return Color4Extensions.FromHex(@"e95353"); + return Color4Extensions.FromHex(@"ff5a5a"); } } From 63531a8564c9bd03cacb53c9038c791fc18caa11 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:59:44 +0900 Subject: [PATCH 344/387] Add date played to score panel --- .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 7a50a96fe7..dcd988a247 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -204,6 +204,13 @@ namespace osu.Game.Screens.Ranking.Expanded } } } + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), + Text = $"Played on {score.Date.ToLocalTime():g}" } } }; From 1d211cb56354056b4068d82ce252b8b0d74525bc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 18:28:42 +0900 Subject: [PATCH 345/387] Make score panel scroll if off-screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 0952ba1f70..803b33a998 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.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 System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -135,7 +136,7 @@ namespace osu.Game.Screens.Ranking protected override void Update() { base.Update(); - content.Height = DrawHeight; + content.Height = Math.Max(768, DrawHeight); } } } From b342e25c9d3fd36fa5c6cc0e9db7b1e28c82eade Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 00:26:49 +0900 Subject: [PATCH 346/387] Add testflight distribution step automation --- fastlane/Fastfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 510b53054b..f895c465d2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -99,6 +99,8 @@ platform :ios do pilot( wait_processing_interval: 1800, changelog: changelog, + groups: ['osu! supporters', 'public'], + distribute_external: true, ipa: './osu.iOS/bin/iPhone/Release/osu.iOS.ipa' ) end From 1c0c26985280e634d5dab81bf8b0cc5361d8d0c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 00:34:24 +0900 Subject: [PATCH 347/387] Reduce allocations of followpoints by reusing existing --- .../Connections/FollowPointConnection.cs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 3e9c0f341b..d0935e46f7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -88,8 +88,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections private void refresh() { - ClearInternal(); - OsuHitObject osuStart = Start.HitObject; double startTime = osuStart.GetEndTime(); @@ -116,6 +114,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double? firstTransformStartTime = null; double finalTransformEndTime = startTime; + int point = 0; + for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) { float fraction = (float)d / distance; @@ -126,13 +126,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections FollowPoint fp; - AddInternal(fp = new FollowPoint + if (InternalChildren.Count > point) { - Position = pointStartPosition, - Rotation = rotation, - Alpha = 0, - Scale = new Vector2(1.5f * osuEnd.Scale), - }); + fp = (FollowPoint)InternalChildren[point]; + fp.ClearTransforms(); + } + else + AddInternal(fp = new FollowPoint()); + + fp.Position = pointStartPosition; + fp.Rotation = rotation; + fp.Alpha = 0; + fp.Scale = new Vector2(1.5f * osuEnd.Scale); if (firstTransformStartTime == null) firstTransformStartTime = fadeInTime; @@ -146,8 +151,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections finalTransformEndTime = fadeOutTime + osuEnd.TimeFadeIn; } + + point++; } + int excessPoints = InternalChildren.Count - point; + for (int i = 0; i < excessPoints; i++) + RemoveInternal(InternalChildren[^1]); + // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. LifetimeStart = firstTransformStartTime ?? startTime; LifetimeEnd = finalTransformEndTime; From 463dde1fc40206a8e7ee3d59ebecdb6186a2f1e4 Mon Sep 17 00:00:00 2001 From: Fuewburvpoa Date: Wed, 18 Mar 2020 21:04:38 +0200 Subject: [PATCH 348/387] Tests for BeatDivisorControl --- .../Editor/TestSceneBeatDivisorControl.cs | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index 7531a7be2c..2ee5e649a8 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Screens.Edit; @@ -11,19 +12,66 @@ using osuTK; namespace osu.Game.Tests.Visual.Editor { - public class TestSceneBeatDivisorControl : OsuTestScene + public class TestSceneBeatDivisorControl : ManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(BindableBeatDivisor) }; + private BeatDivisorControl beatDivisorControl; + private BindableBeatDivisor bindableBeatDivisor; [BackgroundDependencyLoader] private void load() { - Child = new BeatDivisorControl(new BindableBeatDivisor()) + Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(90, 90) }; } + + [Test] + public void TestBindableBeatDivisor() + { + AddStep("Reset", () => reset()); + AddRepeatStep("Move previous", () => bindableBeatDivisor.Previous(), 4); + AddAssert("Position at 4", () => bindableBeatDivisor.Value == 4); + AddRepeatStep("Move next", () => bindableBeatDivisor.Next(), 3); + AddAssert("Position at 12", () => bindableBeatDivisor.Value == 12); + } + + [Test] + public void TestMouseInput() + { + AddStep("Reset", () => reset()); + AddStep("Move to marker", () => + { + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); + InputManager.PressButton(osuTK.Input.MouseButton.Left); + }); + AddStep("Mote to divisor 8", () => + { + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(0, -18)); + InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); + }); + AddAssert("Position at 8", () => bindableBeatDivisor.Value == 8); + AddStep("Prepare to move marker", () => { InputManager.PressButton(osuTK.Input.MouseButton.Left); }); + AddStep("Trigger marker jump", () => + { + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(30, -18)); + }); + AddStep("Move to divisor ~10", () => + { + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(10, -18)); + InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); + }); + AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); + + } + + private void reset() + { + bindableBeatDivisor.Value = 16; + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(beatDivisorControl.Width, beatDivisorControl.Height)); + } } } From d5541dfc651635eecd650b4619000760f8f2fa97 Mon Sep 17 00:00:00 2001 From: Fuewburvpoa Date: Wed, 18 Mar 2020 21:06:14 +0200 Subject: [PATCH 349/387] Codefactor fix --- osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index 2ee5e649a8..b8cefdb841 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -65,7 +65,6 @@ namespace osu.Game.Tests.Visual.Editor InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); }); AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); - } private void reset() From 4bda520695e37a5753a667289e6ac7f2c31147d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Mar 2020 21:54:17 +0100 Subject: [PATCH 350/387] Use [SetUp] instead of reset method --- .../Editor/TestSceneBeatDivisorControl.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index b8cefdb841..dc41f7d92b 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; @@ -18,21 +17,20 @@ namespace osu.Game.Tests.Visual.Editor private BeatDivisorControl beatDivisorControl; private BindableBeatDivisor bindableBeatDivisor; - [BackgroundDependencyLoader] - private void load() + [SetUp] + public void SetUp() => Schedule(() => { - Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor()) + Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(90, 90) }; - } + }); [Test] public void TestBindableBeatDivisor() { - AddStep("Reset", () => reset()); AddRepeatStep("Move previous", () => bindableBeatDivisor.Previous(), 4); AddAssert("Position at 4", () => bindableBeatDivisor.Value == 4); AddRepeatStep("Move next", () => bindableBeatDivisor.Next(), 3); @@ -42,7 +40,6 @@ namespace osu.Game.Tests.Visual.Editor [Test] public void TestMouseInput() { - AddStep("Reset", () => reset()); AddStep("Move to marker", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); @@ -66,11 +63,5 @@ namespace osu.Game.Tests.Visual.Editor }); AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); } - - private void reset() - { - bindableBeatDivisor.Value = 16; - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(beatDivisorControl.Width, beatDivisorControl.Height)); - } } } From 1d3cac4cdc5e61e9148c69f5c567ad135c04d6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Mar 2020 21:55:35 +0100 Subject: [PATCH 351/387] Eliminate osuTK.Input namespace qualifications --- .../Visual/Editor/TestSceneBeatDivisorControl.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index dc41f7d92b..5a441f568a 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.Editor { @@ -43,15 +44,15 @@ namespace osu.Game.Tests.Visual.Editor AddStep("Move to marker", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); - InputManager.PressButton(osuTK.Input.MouseButton.Left); + InputManager.PressButton(MouseButton.Left); }); AddStep("Mote to divisor 8", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(0, -18)); - InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); }); AddAssert("Position at 8", () => bindableBeatDivisor.Value == 8); - AddStep("Prepare to move marker", () => { InputManager.PressButton(osuTK.Input.MouseButton.Left); }); + AddStep("Prepare to move marker", () => { InputManager.PressButton(MouseButton.Left); }); AddStep("Trigger marker jump", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(30, -18)); @@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.Editor AddStep("Move to divisor ~10", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(10, -18)); - InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); }); AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); } From 78bdf5cf9150c851c74433113ca433fa069830b2 Mon Sep 17 00:00:00 2001 From: Fuewburvpoa Date: Wed, 18 Mar 2020 23:03:58 +0200 Subject: [PATCH 352/387] InspectCode fixes --- osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index b8cefdb841..38b33acc32 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Editor [Test] public void TestBindableBeatDivisor() { - AddStep("Reset", () => reset()); + AddStep("Reset", reset); AddRepeatStep("Move previous", () => bindableBeatDivisor.Previous(), 4); AddAssert("Position at 4", () => bindableBeatDivisor.Value == 4); AddRepeatStep("Move next", () => bindableBeatDivisor.Next(), 3); @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Editor [Test] public void TestMouseInput() { - AddStep("Reset", () => reset()); + AddStep("Reset", reset); AddStep("Move to marker", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.Editor InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); }); AddAssert("Position at 8", () => bindableBeatDivisor.Value == 8); - AddStep("Prepare to move marker", () => { InputManager.PressButton(osuTK.Input.MouseButton.Left); }); + AddStep("Prepare to move marker", () => InputManager.PressButton(osuTK.Input.MouseButton.Left)); AddStep("Trigger marker jump", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(30, -18)); From 23338a6c82ed4f14b5e8e4cab0dafd96fb371deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Mar 2020 22:23:06 +0100 Subject: [PATCH 353/387] Adjust test implementation * Use slider bar and slider marker coordinates in manual tests instead of hard-coded offsets. * Reword test steps slightly for greater clarity. --- .../Editor/TestSceneBeatDivisorControl.cs | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index 5a441f568a..746b2c99aa 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -18,6 +22,9 @@ namespace osu.Game.Tests.Visual.Editor private BeatDivisorControl beatDivisorControl; private BindableBeatDivisor bindableBeatDivisor; + private SliderBar tickSliderBar; + private EquilateralTriangle tickMarkerHead; + [SetUp] public void SetUp() => Schedule(() => { @@ -27,42 +34,52 @@ namespace osu.Game.Tests.Visual.Editor Origin = Anchor.Centre, Size = new Vector2(90, 90) }; + + tickSliderBar = beatDivisorControl.ChildrenOfType>().Single(); + tickMarkerHead = tickSliderBar.ChildrenOfType().Single(); }); [Test] public void TestBindableBeatDivisor() { - AddRepeatStep("Move previous", () => bindableBeatDivisor.Previous(), 4); - AddAssert("Position at 4", () => bindableBeatDivisor.Value == 4); - AddRepeatStep("Move next", () => bindableBeatDivisor.Next(), 3); - AddAssert("Position at 12", () => bindableBeatDivisor.Value == 12); + AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 4); + AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4); + AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 3); + AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 12); } [Test] public void TestMouseInput() { - AddStep("Move to marker", () => + AddStep("hold marker", () => { - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); + InputManager.MoveMouseTo(tickMarkerHead.ScreenSpaceDrawQuad.Centre); InputManager.PressButton(MouseButton.Left); }); - AddStep("Mote to divisor 8", () => + AddStep("move to 8 and release", () => { - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(0, -18)); + InputManager.MoveMouseTo(tickSliderBar.ScreenSpaceDrawQuad.Centre); InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Position at 8", () => bindableBeatDivisor.Value == 8); - AddStep("Prepare to move marker", () => { InputManager.PressButton(MouseButton.Left); }); - AddStep("Trigger marker jump", () => + AddAssert("divisor is 8", () => bindableBeatDivisor.Value == 8); + AddStep("hold marker", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move to 16", () => InputManager.MoveMouseTo(getPositionForDivisor(16))); + AddStep("move to ~10 and release", () => { - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(30, -18)); - }); - AddStep("Move to divisor ~10", () => - { - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(10, -18)); + InputManager.MoveMouseTo(getPositionForDivisor(10)); InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); + AddAssert("divisor clamped to 8", () => bindableBeatDivisor.Value == 8); + } + + private Vector2 getPositionForDivisor(int divisor) + { + var relativePosition = (float)Math.Clamp(divisor, 0, 16) / 16; + var sliderDrawQuad = tickSliderBar.ScreenSpaceDrawQuad; + return new Vector2( + sliderDrawQuad.TopLeft.X + sliderDrawQuad.Width * relativePosition, + sliderDrawQuad.Centre.Y + ); } } } From c50784da934163e38991d837a7d0c670274c6b7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 11:58:52 +0900 Subject: [PATCH 354/387] Show 'D' rank badge on accuracy circle --- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 10 ++++++++++ .../Ranking/Expanded/Accuracy/AccuracyCircle.cs | 1 + 2 files changed, 11 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index d0b9d43f51..0781cba924 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -32,6 +32,16 @@ namespace osu.Game.Tests.Visual.Ranking typeof(SmoothCircularProgress) }; + [Test] + public void TestLowDRank() + { + var score = createScore(); + score.Accuracy = 0.2; + score.Rank = ScoreRank.D; + + addCircleStep(score); + } + [Test] public void TestDRank() { diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 2d76a7c3b0..ee53ee9879 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -196,6 +196,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy new RankBadge(0.9f, ScoreRank.A), new RankBadge(0.8f, ScoreRank.B), new RankBadge(0.7f, ScoreRank.C), + new RankBadge(0.35f, ScoreRank.D), } }, rankText = new RankText(score.Rank) From 17c3455b36b9ee38a2116556d62de503cf2232fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:10:54 +0900 Subject: [PATCH 355/387] Fix potentially invalid push in player while already exiting --- osu.Game/Screens/Play/Player.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 32261efd4e..efce2e05ce 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -387,6 +387,10 @@ namespace osu.Game.Screens.Play private void onCompletion() { + // screen may be in the exiting transition phase. + if (!this.IsCurrentScreen()) + return; + // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed || completionProgressDelegate != null) return; @@ -581,7 +585,7 @@ namespace osu.Game.Screens.Play if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed) { // proceed to result screen if beatmap already finished playing - scheduleGotoRanking(); + completionProgressDelegate.RunTask(); return true; } @@ -623,7 +627,12 @@ namespace osu.Game.Screens.Play { var score = CreateScore(); if (DrawableRuleset.ReplayScore == null) - scoreManager.Import(score).ContinueWith(_ => Schedule(() => this.Push(CreateResults(score)))); + scoreManager.Import(score).ContinueWith(_ => Schedule(() => + { + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(score)); + })); else this.Push(CreateResults(score)); }); From 94c3ffb6e508226da53b1f6cc786b7dcbea928fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:26:24 +0900 Subject: [PATCH 356/387] Fix slider ticks contributing to accuracy --- osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 6 +++--- .../TestSceneSliderInput.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- .../Objects/Drawables/DrawableSlider.cs | 10 +++++----- ...bleRepeatPoint.cs => DrawableSliderRepeat.cs} | 16 ++++++++-------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++-- .../{RepeatPoint.cs => SliderRepeatPoint.cs} | 13 ++++++++++--- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 11 +++++++++-- 10 files changed, 41 insertions(+), 27 deletions(-) rename osu.Game.Rulesets.Osu/Objects/Drawables/{DrawableRepeatPoint.cs => DrawableSliderRepeat.cs} (88%) rename osu.Game.Rulesets.Osu/Objects/{RepeatPoint.cs => SliderRepeatPoint.cs} (76%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index defd3a6f22..1c0dd27e69 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(DrawableSliderTick), typeof(DrawableSliderTail), typeof(DrawableSliderHead), - typeof(DrawableRepeatPoint), + typeof(DrawableSliderRepeat), typeof(DrawableOsuHitObject) }; @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("head samples updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle)); AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples)); - AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); + AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("head samples not updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle)); AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples)); - AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); + AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 94df239267..21244f0e9c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(SliderBall), typeof(DrawableSlider), typeof(DrawableSliderTick), - typeof(DrawableRepeatPoint), + typeof(DrawableSliderRepeat), typeof(DrawableOsuHitObject), typeof(DrawableSliderHead), typeof(DrawableSliderTail), diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index bc5f79331f..c1fc589798 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods return; slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); - slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); + slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); foreach (var point in slider.Path.ControlPoints) point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 41daef1f38..44dba7715a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableSliderHead _: case DrawableSliderTail _: case DrawableSliderTick _: - case DrawableRepeatPoint _: + case DrawableSliderRepeat _: return; default: diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index cc2f4c3f70..fe7c70c52c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods // Wiggle the repeat points with the slider instead of independently. // Also fixes an issue with repeat points being positioned incorrectly. - if (osuObject is RepeatPoint) + if (osuObject is SliderRepeatPoint) return; Random objRand = new Random((int)osuObject.StartTime); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7403649184..cb7005cb17 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Container headContainer; private readonly Container tailContainer; private readonly Container tickContainer; - private readonly Container repeatContainer; + private readonly Container repeatContainer; private readonly Slider slider; @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, + repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s, this) { GetInitialHitAction = () => HeadCircle.HitAction, @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables tickContainer.Add(tick); break; - case DrawableRepeatPoint repeat: + case DrawableSliderRepeat repeat: repeatContainer.Add(repeat); break; } @@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case SliderTick tick: return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position }; - case RepeatPoint repeat: - return new DrawableRepeatPoint(repeat, this) { Position = repeat.Position - slider.Position }; + case SliderRepeatPoint repeat: + return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - slider.Position }; } return base.CreateNestedHitObject(hitObject); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs similarity index 88% rename from osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs rename to osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 8fdcd060e7..3336188068 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -14,19 +14,19 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableRepeatPoint : DrawableOsuHitObject, ITrackSnaking + public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking { - private readonly RepeatPoint repeatPoint; + private readonly SliderRepeatPoint sliderRepeatPoint; private readonly DrawableSlider drawableSlider; private double animDuration; private readonly Drawable scaleContainer; - public DrawableRepeatPoint(RepeatPoint repeatPoint, DrawableSlider drawableSlider) - : base(repeatPoint) + public DrawableSliderRepeat(SliderRepeatPoint sliderRepeatPoint, DrawableSlider drawableSlider) + : base(sliderRepeatPoint) { - this.repeatPoint = repeatPoint; + this.sliderRepeatPoint = sliderRepeatPoint; this.drawableSlider = drawableSlider; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -48,13 +48,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (repeatPoint.StartTime <= Time.Current) + if (sliderRepeatPoint.StartTime <= Time.Current) ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss); } protected override void UpdateInitialTransforms() { - animDuration = Math.Min(300, repeatPoint.SpanDuration); + animDuration = Math.Min(300, sliderRepeatPoint.SpanDuration); this.Animate( d => d.FadeIn(animDuration), @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { - bool isRepeatAtEnd = repeatPoint.RepeatIndex % 2 == 0; + bool isRepeatAtEnd = sliderRepeatPoint.RepeatIndex % 2 == 0; List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 77f8ec6cc8..04546b2216 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Repeat: - AddNested(new RepeatPoint + AddNested(new SliderRepeatPoint { RepeatIndex = e.SpanIndex, SpanDuration = SpanDuration, @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Objects foreach (var tick in NestedHitObjects.OfType()) tick.Samples = sampleList; - foreach (var repeat in NestedHitObjects.OfType()) + foreach (var repeat in NestedHitObjects.OfType()) repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1); if (HeadCircle != null) diff --git a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs similarity index 76% rename from osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs rename to osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs index a277517f9f..797383910f 100644 --- a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class RepeatPoint : OsuHitObject + public class SliderRepeatPoint : OsuHitObject { public int RepeatIndex { get; set; } public double SpanDuration { get; set; } @@ -28,8 +28,15 @@ namespace osu.Game.Rulesets.Osu.Objects TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); } - public override Judgement CreateJudgement() => new OsuJudgement(); - protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override Judgement CreateJudgement() => new SliderRepeatPointJudgement(); + + public class SliderRepeatPointJudgement : OsuJudgement + { + public override bool IsBonus => true; + + protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index a49f4cef8b..212a84c04a 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -30,8 +30,15 @@ namespace osu.Game.Rulesets.Osu.Objects TimePreempt = (StartTime - SpanStartTime) / 2 + offset; } - public override Judgement CreateJudgement() => new OsuJudgement(); - protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override Judgement CreateJudgement() => new SliderTickJudgement(); + + public class SliderTickJudgement : OsuJudgement + { + public override bool IsBonus => true; + + protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0; + } } } From 855f0a42530635fad333ed4962aed98079c07850 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:38:49 +0900 Subject: [PATCH 357/387] Fix bracket style --- osu.Game/Screens/Play/Player.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index efce2e05ce..a120963abd 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -626,13 +626,16 @@ namespace osu.Game.Screens.Play completionProgressDelegate = Schedule(delegate { var score = CreateScore(); + if (DrawableRuleset.ReplayScore == null) + { scoreManager.Import(score).ContinueWith(_ => Schedule(() => { // screen may be in the exiting transition phase. if (this.IsCurrentScreen()) this.Push(CreateResults(score)); })); + } else this.Push(CreateResults(score)); }); From 08b5ab8ec43c01eace7d1639fbb92dd2332bea2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:42:02 +0900 Subject: [PATCH 358/387] SliderRepeatPoint -> SliderRepeat --- osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/DrawableSliderRepeat.cs | 14 +++++++------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++-- .../{SliderRepeatPoint.cs => SliderRepeat.cs} | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) rename osu.Game.Rulesets.Osu/Objects/{SliderRepeatPoint.cs => SliderRepeat.cs} (91%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 1c0dd27e69..a201364de4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("head samples updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle)); AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples)); - AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); + AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("head samples not updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle)); AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples)); - AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); + AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index c1fc589798..cf6677a55d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods return; slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); - slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); + slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); foreach (var point in slider.Path.ControlPoints) point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index fe7c70c52c..297a0fea79 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods // Wiggle the repeat points with the slider instead of independently. // Also fixes an issue with repeat points being positioned incorrectly. - if (osuObject is SliderRepeatPoint) + if (osuObject is SliderRepeat) return; Random objRand = new Random((int)osuObject.StartTime); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index cb7005cb17..8b8a0ff22a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case SliderTick tick: return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position }; - case SliderRepeatPoint repeat: + case SliderRepeat repeat: return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - slider.Position }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 3336188068..b9cee71ca1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -16,17 +16,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking { - private readonly SliderRepeatPoint sliderRepeatPoint; + private readonly SliderRepeat sliderRepeat; private readonly DrawableSlider drawableSlider; private double animDuration; private readonly Drawable scaleContainer; - public DrawableSliderRepeat(SliderRepeatPoint sliderRepeatPoint, DrawableSlider drawableSlider) - : base(sliderRepeatPoint) + public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) + : base(sliderRepeat) { - this.sliderRepeatPoint = sliderRepeatPoint; + this.sliderRepeat = sliderRepeat; this.drawableSlider = drawableSlider; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -48,13 +48,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (sliderRepeatPoint.StartTime <= Time.Current) + if (sliderRepeat.StartTime <= Time.Current) ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss); } protected override void UpdateInitialTransforms() { - animDuration = Math.Min(300, sliderRepeatPoint.SpanDuration); + animDuration = Math.Min(300, sliderRepeat.SpanDuration); this.Animate( d => d.FadeIn(animDuration), @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { - bool isRepeatAtEnd = sliderRepeatPoint.RepeatIndex % 2 == 0; + bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 04546b2216..28706b07f3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Repeat: - AddNested(new SliderRepeatPoint + AddNested(new SliderRepeat { RepeatIndex = e.SpanIndex, SpanDuration = SpanDuration, @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Objects foreach (var tick in NestedHitObjects.OfType()) tick.Samples = sampleList; - foreach (var repeat in NestedHitObjects.OfType()) + foreach (var repeat in NestedHitObjects.OfType()) repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1); if (HeadCircle != null) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs similarity index 91% rename from osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs rename to osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index 797383910f..a8fd3764c5 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class SliderRepeatPoint : OsuHitObject + public class SliderRepeat : OsuHitObject { public int RepeatIndex { get; set; } public double SpanDuration { get; set; } @@ -30,9 +30,9 @@ namespace osu.Game.Rulesets.Osu.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderRepeatPointJudgement(); + public override Judgement CreateJudgement() => new SliderRepeatJudgement(); - public class SliderRepeatPointJudgement : OsuJudgement + public class SliderRepeatJudgement : OsuJudgement { public override bool IsBonus => true; From 114b46c4f02a9a8d92c9e614722a70a9877971d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:44:48 +0900 Subject: [PATCH 359/387] Change slider tail to give repeat judgement; slider itself to give none (managed by head already) --- .../Objects/Drawables/DrawableSlider.cs | 21 +++---------------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 +-- .../Objects/SliderTailCircle.cs | 4 ++-- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 8b8a0ff22a..2d5b9d874c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -6,13 +6,11 @@ using osuTK; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; -using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -26,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly SliderBall Ball; public readonly SkinnableDrawable Body; + public override bool DisplayResult => false; + private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; private readonly Container headContainer; @@ -193,22 +193,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < slider.EndTime) return; - ApplyResult(r => - { - var judgementsCount = NestedHitObjects.Count; - var judgementsHit = NestedHitObjects.Count(h => h.IsHit); - - var hitFraction = (double)judgementsHit / judgementsCount; - - if (hitFraction == 1 && HeadCircle.Result.Type == HitResult.Great) - r.Type = HitResult.Great; - else if (hitFraction >= 0.5 && HeadCircle.Result.Type >= HitResult.Good) - r.Type = HitResult.Good; - else if (hitFraction > 0) - r.Type = HitResult.Meh; - else - r.Type = HitResult.Miss; - }); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 28706b07f3..3812a62a25 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -11,7 +11,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects @@ -233,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Objects private IList getNodeSamples(int nodeIndex) => nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples; - public override Judgement CreateJudgement() => new OsuJudgement(); + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 127c36fcc0..c11e20c9e7 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -22,8 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects pathVersion.BindValueChanged(_ => Position = slider.EndPosition); } - public override Judgement CreateJudgement() => new IgnoreJudgement(); - protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override Judgement CreateJudgement() => new SliderRepeat.SliderRepeatJudgement(); } } From 3489514b65f6462d27f80dcf9db6b5039d8d527e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 17:15:53 +0900 Subject: [PATCH 360/387] Fix tests asserting incorrectly --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 21244f0e9c..67e1b77770 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -327,7 +327,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Tracking dropped", assertMidSliderJudgementFail); } - private bool assertGreatJudge() => judgementResults.Last().Type == HitResult.Great; + private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == HitResult.Great); private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss; From 8f9e97b4ccbec6e9de72dba2fca7bfe29e1b7b88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 18:07:39 +0900 Subject: [PATCH 361/387] Fix carousel not remembering last selection correctly --- osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs index 045c682dc3..6ce12f7b89 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs @@ -104,7 +104,8 @@ namespace osu.Game.Screens.Select.Carousel private void updateSelected(CarouselItem newSelection) { - LastSelected = newSelection; + if (newSelection != null) + LastSelected = newSelection; updateSelectedIndex(); } From 0c1f385d5aae66ed9f4735fd154487481efc397f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 18:19:10 +0900 Subject: [PATCH 362/387] Add OsuIgnoreJudgement to get correct result type --- .../Judgements/OsuIgnoreJudgement.cs | 16 ++++++++++++++++ .../Objects/Drawables/DrawableSliderTail.cs | 2 +- .../Objects/Drawables/DrawableSliderTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 ++- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs new file mode 100644 index 0000000000..e528f65dca --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuIgnoreJudgement : OsuJudgement + { + public override bool AffectsCombo => false; + + protected override int NumericResultFor(HitResult result) => 0; + + protected override double HealthIncreaseFor(HitResult result) => 0; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 21a3a0d236..29a4929c1b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered && timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss); } private void updatePosition() => Position = HitObject.Position - slider.Position; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 60b5c335d6..66eb60aa28 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 3812a62a25..db1f46d8e2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -11,6 +11,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects @@ -232,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Objects private IList getNodeSamples(int nodeIndex) => nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples; - public override Judgement CreateJudgement() => new IgnoreJudgement(); + public override Judgement CreateJudgement() => new OsuIgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } From 3a50c4bb51bf0377ad51e830656ef2a24c32417f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 18:58:22 +0900 Subject: [PATCH 363/387] Update tests --- .../SongSelect/TestSceneBeatmapCarousel.cs | 86 +++++++++++++++---- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 8df75c78f5..c2534e2cc7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -419,7 +419,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestCarouselRootIsRandom() + public void TestCarouselRemembersSelection() { List manySets = new List(); @@ -429,12 +429,74 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(manySets); advanceSelection(direction: 1, diff: false); - checkNonmatchingFilter(); - checkNonmatchingFilter(); - checkNonmatchingFilter(); - checkNonmatchingFilter(); - checkNonmatchingFilter(); - AddAssert("Selection was random", () => eagerSelectedIDs.Count > 1); + + for (int i = 0; i < 5; i++) + { + AddStep("Toggle non-matching filter", () => + { + carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + }); + + AddStep("Restore no filter", () => + { + carousel.Filter(new FilterCriteria(), false); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); + }); + } + + // always returns to same selection as long as it's available. + AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); + } + + [Test] + public void TestRandomFallbackOnNonMatchingPrevious() + { + List manySets = new List(); + + AddStep("populate maps", () => + { + for (int i = 0; i < 10; i++) + { + var set = createTestBeatmapSet(i); + + foreach (var b in set.Beatmaps) + { + // all taiko except for first + int ruleset = i > 0 ? 1 : 0; + + b.Ruleset = rulesets.GetRuleset(ruleset); + b.RulesetID = ruleset; + } + + manySets.Add(set); + } + }); + + loadBeatmaps(manySets); + + for (int i = 0; i < 10; i++) + { + AddStep("Reset filter", () => carousel.Filter(new FilterCriteria(), false)); + + AddStep("select first beatmap", () => carousel.SelectBeatmap(manySets.First().Beatmaps.First())); + + AddStep("Toggle non-matching filter", () => + { + carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + }); + + AddAssert("selection lost", () => carousel.SelectedBeatmap == null); + + AddStep("Restore different ruleset filter", () => + { + carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); + }); + + AddAssert("selection changed", () => carousel.SelectedBeatmap != manySets.First().Beatmaps.First()); + } + + AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2); } [Test] @@ -593,16 +655,6 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection is visible", selectedBeatmapVisible); } - private void checkNonmatchingFilter() - { - AddStep("Toggle non-matching filter", () => - { - carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false); - carousel.Filter(new FilterCriteria(), false); - eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); - }); - } - private BeatmapSetInfo createTestBeatmapSet(int id) { return new BeatmapSetInfo From 43c1f27f2490ee304e2347473f6cbc2bd3b5de36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 20:19:50 +0900 Subject: [PATCH 364/387] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 942970c890..7e17f9da16 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 54f1ad2845..46d17bcf05 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 816a430b52..9cc9792ecf 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 549cec80d6d62ecd55d7a8dbfc55363c6ea7f273 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Mar 2020 02:47:45 +0900 Subject: [PATCH 365/387] Reduce app store processing wait interval in line with faster processing time --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f895c465d2..4fd0e5e8c7 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -97,7 +97,7 @@ platform :ios do changelog.gsub!('$BUILD_ID', options[:build]) pilot( - wait_processing_interval: 1800, + wait_processing_interval: 900, changelog: changelog, groups: ['osu! supporters', 'public'], distribute_external: true, From be4a97c2894282c794c763f269dcdb991aacf512 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Mar 2020 13:01:24 +0900 Subject: [PATCH 366/387] Correctly bypass last selected when it is filtered --- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9f8b201eff..389ae918b9 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -333,8 +333,7 @@ namespace osu.Game.Screens.Select else set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); - var visibleBeatmaps = set.Beatmaps.Where(s => !s.Filtered.Value).ToList(); - select(visibleBeatmaps[RNG.Next(visibleBeatmaps.Count)]); + select(set); return true; } @@ -756,7 +755,7 @@ namespace osu.Game.Screens.Select protected override void PerformSelection() { - if (LastSelected == null) + if (LastSelected == null || LastSelected.Filtered.Value) carousel.SelectNextRandom(); else base.PerformSelection(); From 9b60b535e596c051c353400f5feeb68e4562fa5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Mar 2020 15:01:26 +0900 Subject: [PATCH 367/387] Fix selection not occurring when switching from empty ruleset on first load --- .../SongSelect/TestSceneBeatmapCarousel.cs | 32 +++++++++++++++++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++- osu.Game/Screens/Select/SongSelect.cs | 3 ++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 8df75c78f5..bd26120da9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -227,6 +227,32 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } + [Test] + public void TestSelectionEnteringFromEmptyRuleset() + { + var sets = new List(); + + AddStep("Create beatmaps for taiko only", () => + { + var rulesetBeatmapSet = createTestBeatmapSet(1); + var taikoRuleset = rulesets.AvailableRulesets.ElementAt(1); + rulesetBeatmapSet.Beatmaps.ForEach(b => + { + b.Ruleset = taikoRuleset; + b.RulesetID = 1; + }); + + sets.Add(rulesetBeatmapSet); + }); + + loadBeatmaps(sets, () => new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }); + + AddStep("Set non-empty mode filter", () => + carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false)); + + AddAssert("Something is selected", () => carousel.SelectedBeatmap != null); + } + /// /// Test sorting /// @@ -399,7 +425,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); - AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap == null); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap.RulesetID == 0); AddStep("remove mixed set", () => { @@ -484,7 +510,7 @@ namespace osu.Game.Tests.Visual.SongSelect checkVisibleItemCount(true, 15); } - private void loadBeatmaps(List beatmapSets = null) + private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null) { createCarousel(); @@ -499,7 +525,7 @@ namespace osu.Game.Tests.Visual.SongSelect bool changed = false; AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () => { - carousel.Filter(new FilterCriteria()); + carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria()); carousel.BeatmapSetsChanged = () => changed = true; carousel.BeatmapSets = beatmapSets; }); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9f8b201eff..6c6f9a0e79 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -751,13 +751,17 @@ namespace osu.Game.Screens.Select public CarouselRoot(BeatmapCarousel carousel) { + // root should always remaing selected. if not, PerformSelection will not be called. + State.Value = CarouselItemState.Selected; + State.ValueChanged += state => State.Value = CarouselItemState.Selected; + this.carousel = carousel; } protected override void PerformSelection() { if (LastSelected == null) - carousel.SelectNextRandom(); + carousel?.SelectNextRandom(); else base.PerformSelection(); } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 11c680bdb0..b6ec40ab88 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -150,6 +150,7 @@ namespace osu.Game.Screens.Select }, Child = Carousel = new BeatmapCarousel { + AllowSelection = false, // delay any selection until our bindables are ready to make a good choice. Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, @@ -655,6 +656,8 @@ namespace osu.Game.Screens.Select { bindBindables(); + Carousel.AllowSelection = true; + // If a selection was already obtained, do not attempt to update the selected beatmap. if (Carousel.SelectedBeatmapSet != null) return; From 8136ea561e689786be2b5938fec47bdffc495d8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Mar 2020 15:02:13 +0900 Subject: [PATCH 368/387] Fix a couple of broken tests --- .../Background/TestSceneUserDimBackgrounds.cs | 1 + .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index b51555db3e..1ddc1326d5 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -278,6 +278,7 @@ namespace osu.Game.Tests.Visual.Background private void setupUserSettings() { + AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen()); AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmap != null); AddStep("Set default user settings", () => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index bd26120da9..5b4c57e28f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -432,13 +432,18 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.RemoveBeatmapSet(testMixed); testMixed = null; }); - var testSingle = createTestBeatmapSet(set_count + 2); - testSingle.Beatmaps.ForEach(b => + BeatmapSetInfo testSingle = null; + AddStep("add single ruleset beatmapset", () => { - b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); - b.RulesetID = b.Ruleset.ID ?? 1; + testSingle = createTestBeatmapSet(set_count + 2); + testSingle.Beatmaps.ForEach(b => + { + b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); + b.RulesetID = b.Ruleset.ID ?? 1; + }); + + carousel.UpdateBeatmapSet(testSingle); }); - AddStep("add single ruleset beatmapset", () => carousel.UpdateBeatmapSet(testSingle)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false)); checkNoSelection(); AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle)); From b813c0aff1bff795bb5247caf7dfeac26a1d19a4 Mon Sep 17 00:00:00 2001 From: OctopuSSX Date: Fri, 20 Mar 2020 14:26:26 +0300 Subject: [PATCH 369/387] Don't open profile if it's Autoplay --- osu.Game/Users/Drawables/DrawableAvatar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 93136e88a0..fe44c56ae0 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -68,7 +68,7 @@ namespace osu.Game.Users.Drawables if (!OpenOnClick.Value) return; - if (user != null) + if (user != null && user.Id != 1) game?.ShowUser(user.Id); } From 157f05c3e508a228a9f13e7e454a937faab0b7c9 Mon Sep 17 00:00:00 2001 From: OctopuSSX Date: Fri, 20 Mar 2020 14:35:27 +0300 Subject: [PATCH 370/387] Update osu.Game/Users/Drawables/DrawableAvatar.cs Co-Authored-By: Dean Herbert --- osu.Game/Users/Drawables/DrawableAvatar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index fe44c56ae0..09750c5bfe 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -68,7 +68,7 @@ namespace osu.Game.Users.Drawables if (!OpenOnClick.Value) return; - if (user != null && user.Id != 1) + if (user?.Id > 1) game?.ShowUser(user.Id); } From 29009c85c01ad1729d54f25137e8ae6eb18b6d5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Mar 2020 00:32:53 +0900 Subject: [PATCH 371/387] Fix typo in comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6c6f9a0e79..739f99d72d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -751,7 +751,7 @@ namespace osu.Game.Screens.Select public CarouselRoot(BeatmapCarousel carousel) { - // root should always remaing selected. if not, PerformSelection will not be called. + // root should always remain selected. if not, PerformSelection will not be called. State.Value = CarouselItemState.Selected; State.ValueChanged += state => State.Value = CarouselItemState.Selected; From d8041a0dcbfc5929c3244b686c959fa78a25857a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Mar 2020 02:16:28 +0900 Subject: [PATCH 372/387] Increase sample concurrency to better match stable --- osu.Game/OsuGameBase.cs | 4 ++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 1 + osu.Game/Skinning/LegacySkin.cs | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3c7ab27651..5487bd9320 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -47,6 +47,8 @@ namespace osu.Game { public const string CLIENT_STREAM_NAME = "lazer"; + public const int SAMPLE_CONCURRENCY = 6; + protected OsuConfigManager LocalConfig; protected BeatmapManager BeatmapManager; @@ -153,6 +155,8 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera-Bold"); AddFont(Resources, @"Fonts/Venera-Black"); + Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; + runMigrations(); dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index e624fb80fa..d0a2722f58 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.UI dependencies.Cache(textureStore); localSampleStore = dependencies.Get().GetSampleStore(new NamespacedResourceStore(resources, "Samples")); + localSampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; dependencies.CacheAs(new FallbackSampleStore(localSampleStore, dependencies.Get())); } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 29bcd2e210..c71a321e74 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -52,7 +52,11 @@ namespace osu.Game.Skinning if (storage != null) { - Samples = audioManager?.GetSampleStore(storage); + var samples = audioManager?.GetSampleStore(storage); + if (samples != null) + samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; + + Samples = samples; Textures = new TextureStore(new TextureLoaderStore(storage)); (storage as ResourceStore)?.AddExtension("ogg"); From d241f7c55fece40263c582e0dd6f89ceb3a60d2e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Mar 2020 20:32:55 +0300 Subject: [PATCH 373/387] Better variable naming --- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 55a5081435..c2f0917e29 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -171,9 +171,9 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var groupedUsers = onlineStatusControl.Current.Value?.Users ?? new List(); + var usersInCurrentGroup = onlineStatusControl.Current.Value?.Users ?? new List(); - var sortedUsers = sortUsers(groupedUsers); + var sortedUsers = sortUsers(usersInCurrentGroup); LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); } From 2b0c267cb902a8c9667a7e6afba57fb351fb1deb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Mar 2020 20:37:21 +0300 Subject: [PATCH 374/387] Expose Fetch method --- osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs | 5 +---- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index e6b2a41c1c..1d8238dd40 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -10,7 +10,6 @@ using osu.Game.Users; using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; -using osu.Game.Online.API; namespace osu.Game.Tests.Visual.Online { @@ -28,9 +27,6 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - [Resolved] - private IAPIProvider api { get; set; } - private FriendsLayout layout; [SetUp] @@ -52,6 +48,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnline() { + AddStep("Fetch online", () => layout?.Fetch()); } private List getUsers() => new List diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index c2f0917e29..94c8230d8e 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -25,8 +25,6 @@ namespace osu.Game.Overlays.Dashboard.Friends get => users; set { - request?.Cancel(); - users = value; onlineStatusControl.Populate(value); @@ -152,7 +150,10 @@ namespace osu.Game.Overlays.Dashboard.Friends onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); + } + public void Fetch() + { if (!api.IsLoggedIn) return; From 19b6e496efbc0dddc8abb4af66a002e98a130512 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Mar 2020 04:17:04 +0900 Subject: [PATCH 375/387] Fix (very) long spinners degrading in performance due to high transform count --- osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index e3dd2b1b4f..3de30d51d9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces .FadeTo(tracking_alpha, 250, Easing.OutQuint); } - this.RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, Easing.OutExpo); + Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } } } From 9482fc5b9990af7a53611539332e81cc1d5d79d5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Mar 2020 20:13:54 +0300 Subject: [PATCH 376/387] Refactor grouping logic --- .../Dashboard/Friends/FriendsBundle.cs | 11 +++------- .../Dashboard/Friends/FriendsLayout.cs | 20 ++++++++++++++++--- .../Friends/FriendsOnlineStatusControl.cs | 9 ++++++--- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs index 772d9c67a0..d5fad1ffd3 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs @@ -1,23 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using osu.Game.Users; - namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsBundle { public FriendsOnlineStatus Status { get; } - public int Count => Users.Count; + public int Count { get; } - public List Users { get; } - - public FriendsBundle(FriendsOnlineStatus status, List users) + public FriendsBundle(FriendsOnlineStatus status, int count) { Status = status; - Users = users; + Count = count; } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 94c8230d8e..3514bf7ff7 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -172,13 +172,27 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var usersInCurrentGroup = onlineStatusControl.Current.Value?.Users ?? new List(); - - var sortedUsers = sortUsers(usersInCurrentGroup); + var sortedUsers = sortUsers(getUsersInCurrentGroup()); LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); } + private List getUsersInCurrentGroup() + { + switch (onlineStatusControl.Current.Value?.Status) + { + default: + case FriendsOnlineStatus.All: + return users; + + case FriendsOnlineStatus.Offline: + return users.Where(u => !u.IsOnline).ToList(); + + case FriendsOnlineStatus.Online: + return users.Where(u => u.IsOnline).ToList(); + } + } + private void addContentToPlaceholder(Drawable content) { loading.Hide(); diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs index 88035e0a34..c54e9e2a06 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs @@ -15,9 +15,12 @@ namespace osu.Game.Overlays.Dashboard.Friends { Clear(); - AddItem(new FriendsBundle(FriendsOnlineStatus.All, users)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Online, users.Where(u => u.IsOnline).ToList())); - AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, users.Where(u => !u.IsOnline).ToList())); + var userCount = users.Count; + var onlineUsersCount = users.Count(user => user.IsOnline); + + AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount)); Current.Value = Items.FirstOrDefault(); } From 97076325c46f8f734da5fa50cceaab7ea3747cf0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 23 Mar 2020 01:45:13 +0300 Subject: [PATCH 377/387] Fix test scenes using framework-testing-specifc test scene --- .../TestSceneHitCircleArea.cs | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 67b6dac787..394d959d0a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -4,13 +4,13 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Osu.Tests @@ -22,30 +22,25 @@ namespace osu.Game.Rulesets.Osu.Tests private DrawableHitCircle.HitReceptor hitAreaReceptor => drawableHitCircle.HitArea; [SetUp] - public new void SetUp() + public void SetUp() => Schedule(() => { - base.SetUp(); - - Schedule(() => + hitCircle = new HitCircle { - hitCircle = new HitCircle - { - Position = new Vector2(100, 100), - StartTime = Time.Current + 500 - }; + Position = new Vector2(100, 100), + StartTime = Time.Current + 500 + }; - hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - Child = new SkinProvidingContainer(new DefaultSkin()) + Child = new SkinProvidingContainer(new DefaultSkin()) + { + RelativeSizeAxes = Axes.Both, + Child = drawableHitCircle = new DrawableHitCircle(hitCircle) { - RelativeSizeAxes = Axes.Both, - Child = drawableHitCircle = new DrawableHitCircle(hitCircle) - { - Size = new Vector2(100) - } - }; - }); - } + Size = new Vector2(100) + } + }; + }); [Test] public void TestCircleHitCentre() From 0b728f483fb1d2934a629387fe936ff9fba84d8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 10:01:33 +0900 Subject: [PATCH 378/387] Rename base test class to help avoid incorrect reference --- osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs | 2 +- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 2 +- osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs | 2 +- osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs | 2 +- .../Visual/Editor/TestSceneZoomableScrollContainer.cs | 2 +- .../Visual/Gameplay/TestSceneGameplayMenuOverlay.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs | 2 +- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 2 +- osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 2 +- osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs | 2 +- osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs | 2 +- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- .../Visual/UserInterface/TestSceneOsuHoverContainer.cs | 2 +- .../Visual/UserInterface/TestSceneStatefulMenuItem.cs | 2 +- osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs | 2 +- ...tManagerTestScene.cs => OsuManualInputManagerTestScene.cs} | 4 ++-- osu.Game/Tests/Visual/ScreenTestScene.cs | 2 +- osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs | 2 +- 25 files changed, 26 insertions(+), 26 deletions(-) rename osu.Game/Tests/Visual/{ManualInputManagerTestScene.cs => OsuManualInputManagerTestScene.cs} (97%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 394d959d0a..0649989dc0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneHitCircleArea : ManualInputManagerTestScene + public class TestSceneHitCircleArea : OsuManualInputManagerTestScene { private HitCircle hitCircle; private DrawableHitCircle drawableHitCircle; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs index 4af4d5f966..0ae49790cd 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs @@ -23,7 +23,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneOsuDistanceSnapGrid : ManualInputManagerTestScene + public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene { private const double beat_length = 100; private static readonly Vector2 grid_position = new Vector2(512, 384); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 8e73d6152f..f4809b0c9b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -12,7 +12,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneResumeOverlay : ManualInputManagerTestScene + public class TestSceneResumeOverlay : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index b51555db3e..c906f21e22 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -37,7 +37,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Background { [TestFixture] - public class TestSceneUserDimBackgrounds : ManualInputManagerTestScene + public class TestSceneUserDimBackgrounds : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs b/osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs index 55aaeed8bf..4d64c7d35d 100644 --- a/osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs +++ b/osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Components { [TestFixture] - public class TestSceneIdleTracker : ManualInputManagerTestScene + public class TestSceneIdleTracker : OsuManualInputManagerTestScene { private IdleTrackingBox box1; private IdleTrackingBox box2; diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index 746b2c99aa..fd7a5980f3 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -16,7 +16,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Editor { - public class TestSceneBeatDivisorControl : ManualInputManagerTestScene + public class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(BindableBeatDivisor) }; private BeatDivisorControl beatDivisorControl; diff --git a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs index fd248abbc9..19d19c2759 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Editor { - public class TestSceneZoomableScrollContainer : ManualInputManagerTestScene + public class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene { private ZoomableScrollContainer scrollContainer; private Drawable innerBox; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index c1635ffc83..ea3e0c2293 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -18,7 +18,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [Description("player pause/fail screens")] - public class TestSceneGameplayMenuOverlay : ManualInputManagerTestScene + public class TestSceneGameplayMenuOverlay : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseOverlay) }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index fc03dc6ed3..c192a7b0e0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -15,7 +15,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneHUDOverlay : ManualInputManagerTestScene + public class TestSceneHUDOverlay : OsuManualInputManagerTestScene { private HUDOverlay hudOverlay; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs index 0c5ead10cf..235842acc9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs @@ -13,7 +13,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [Description("'Hold to Quit' UI element")] - public class TestSceneHoldForMenuButton : ManualInputManagerTestScene + public class TestSceneHoldForMenuButton : OsuManualInputManagerTestScene { private bool exitAction; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 227ada70fe..593dcd245c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -13,7 +13,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneKeyCounter : ManualInputManagerTestScene + public class TestSceneKeyCounter : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 175f909a5a..4c73065087 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -29,7 +29,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestScenePlayerLoader : ManualInputManagerTestScene + public class TestScenePlayerLoader : OsuManualInputManagerTestScene { private TestPlayerLoader loader; private TestPlayerLoaderContainer container; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 4c5c18f38a..6a0f86fe53 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -14,7 +14,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneSkipOverlay : ManualInputManagerTestScene + public class TestSceneSkipOverlay : OsuManualInputManagerTestScene { private SkipOverlay skip; private int requestCount; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 9fbe8f7ffe..713ba13439 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -20,7 +20,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneDrawableRoomPlaylist : ManualInputManagerTestScene + public class TestSceneDrawableRoomPlaylist : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 0d64eb651f..31afce86ae 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Navigation /// /// A scene which tests full game flow. /// - public abstract class OsuGameTestScene : ManualInputManagerTestScene + public abstract class OsuGameTestScene : OsuManualInputManagerTestScene { private GameHost host; diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 6665452d94..14924dda21 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -22,7 +22,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Online { - public class TestSceneChatOverlay : ManualInputManagerTestScene + public class TestSceneChatOverlay : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index 7b0b644dab..cef04a4c18 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -15,7 +15,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneCommentEditor : ManualInputManagerTestScene + public class TestSceneCommentEditor : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs index d1dde4664a..5b74852259 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneCursors : ManualInputManagerTestScene + public class TestSceneCursors : OsuManualInputManagerTestScene { private readonly MenuCursorContainer menuCursorContainer; private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6]; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 1e5e26e4c5..a812b4dc79 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -27,7 +27,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneDeleteLocalScore : ManualInputManagerTestScene + public class TestSceneDeleteLocalScore : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs index dbef7d1686..396bec51b6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneOsuHoverContainer : ManualInputManagerTestScene + public class TestSceneOsuHoverContainer : OsuManualInputManagerTestScene { private OsuHoverTestContainer hoverContainer; private Box colourContainer; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs index 2ada5b927b..85fea73bf5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs @@ -12,7 +12,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneStatefulMenuItem : ManualInputManagerTestScene + public class TestSceneStatefulMenuItem : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs index 4a104b4a41..37fab75aee 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs @@ -9,7 +9,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneSwitchButton : ManualInputManagerTestScene + public class TestSceneSwitchButton : OsuManualInputManagerTestScene { private SwitchButton switchButton; diff --git a/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs similarity index 97% rename from osu.Game/Tests/Visual/ManualInputManagerTestScene.cs rename to osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index a0af07013c..0da3ae7f87 100644 --- a/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual { - public abstract class ManualInputManagerTestScene : OsuTestScene + public abstract class OsuManualInputManagerTestScene : OsuTestScene { protected override Container Content => content; private readonly Container content; @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual private readonly TriangleButton buttonTest; private readonly TriangleButton buttonLocal; - protected ManualInputManagerTestScene() + protected OsuManualInputManagerTestScene() { base.Content.AddRange(new Drawable[] { diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index d26aacf2bc..33cc00e748 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -11,7 +11,7 @@ namespace osu.Game.Tests.Visual /// /// A test case which can be used to test a screen (that relies on OnEntering being called to execute startup instructions). /// - public abstract class ScreenTestScene : ManualInputManagerTestScene + public abstract class ScreenTestScene : OsuManualInputManagerTestScene { protected readonly OsuScreenStack Stack; diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs index 6565f98666..1176361679 100644 --- a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Tests.Visual { - public abstract class SelectionBlueprintTestScene : ManualInputManagerTestScene + public abstract class SelectionBlueprintTestScene : OsuManualInputManagerTestScene { protected override Container Content => content ?? base.Content; private readonly Container content; From 754d0ca14d43037c745c9a74a5ce8321bb0f195a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Mar 2020 10:28:17 +0900 Subject: [PATCH 379/387] Fix crashes due to ladder re-use --- osu.Game.Tournament.Tests/LadderTestScene.cs | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament.Tests/LadderTestScene.cs b/osu.Game.Tournament.Tests/LadderTestScene.cs index 4477ca8338..b962d035ab 100644 --- a/osu.Game.Tournament.Tests/LadderTestScene.cs +++ b/osu.Game.Tournament.Tests/LadderTestScene.cs @@ -1,10 +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.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Tournament.Models; using osu.Game.Users; @@ -13,8 +15,20 @@ namespace osu.Game.Tournament.Tests [TestFixture] public abstract class LadderTestScene : TournamentTestScene { + [Cached] + protected LadderInfo Ladder { get; private set; } = new LadderInfo(); + [Resolved] - protected LadderInfo Ladder { get; private set; } + private RulesetStore rulesetStore { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + if (Ladder.Ruleset.Value == null) + Ladder.Ruleset.Value = rulesetStore.AvailableRulesets.First(); + + Ruleset.BindTo(Ladder.Ruleset); + } protected override void LoadComplete() { @@ -22,13 +36,8 @@ namespace osu.Game.Tournament.Tests TournamentMatch match = CreateSampleMatch(); - Ladder.Rounds.Clear(); Ladder.Rounds.Add(match.Round.Value); - - Ladder.Matches.Clear(); Ladder.Matches.Add(match); - - Ladder.Teams.Clear(); Ladder.Teams.Add(match.Team1.Value); Ladder.Teams.Add(match.Team2.Value); From bfd643dd1659179186a5d26982b58bcbfcd3d5e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 10:47:27 +0900 Subject: [PATCH 380/387] Rename classes --- .../Visual/Online/TestSceneFriendsLayout.cs | 12 +++++------ .../TestSceneFriendsOnlineStatusControl.cs | 14 ++++++------- .../{FriendsLayout.cs => FriendDisplay.cs} | 20 +++++++++---------- ...ontrol.cs => FriendOnlineStreamControl.cs} | 10 +++++----- .../{FriendsBundle.cs => FriendStream.cs} | 13 +++--------- .../Friends/FriendsOnlineStatusItem.cs | 10 +++++----- .../Dashboard/Friends/OnlineStatus.cs | 12 +++++++++++ 7 files changed, 48 insertions(+), 43 deletions(-) rename osu.Game/Overlays/Dashboard/Friends/{FriendsLayout.cs => FriendDisplay.cs} (94%) rename osu.Game/Overlays/Dashboard/Friends/{FriendsOnlineStatusControl.cs => FriendOnlineStreamControl.cs} (53%) rename osu.Game/Overlays/Dashboard/Friends/{FriendsBundle.cs => FriendStream.cs} (57%) create mode 100644 osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 1d8238dd40..c0a617fe57 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -17,8 +17,8 @@ namespace osu.Game.Tests.Visual.Online { public override IReadOnlyList RequiredTypes => new[] { - typeof(FriendsLayout), - typeof(FriendsOnlineStatusControl), + typeof(FriendDisplay), + typeof(FriendOnlineStreamControl), typeof(UserListToolbar) }; @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private FriendsLayout layout; + private FriendDisplay display; [SetUp] public void Setup() => Schedule(() => @@ -35,20 +35,20 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = layout = new FriendsLayout() + Child = display = new FriendDisplay() }; }); [Test] public void TestOffline() { - AddStep("Populate", () => layout.Users = getUsers()); + AddStep("Populate", () => display.Users = getUsers()); } [Test] public void TestOnline() { - AddStep("Fetch online", () => layout?.Fetch()); + AddStep("Fetch online", () => display?.Fetch()); } private List getUsers() => new List diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index d72818ed89..f6dcf78d55 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -17,20 +17,20 @@ namespace osu.Game.Tests.Visual.UserInterface { public override IReadOnlyList RequiredTypes => new[] { - typeof(FriendsOnlineStatusControl), + typeof(FriendOnlineStreamControl), typeof(FriendsOnlineStatusItem), typeof(OverlayStreamControl<>), typeof(OverlayStreamItem<>), - typeof(FriendsBundle) + typeof(FriendStream) }; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private FriendsOnlineStatusControl control; + private FriendOnlineStreamControl control; [SetUp] - public void SetUp() => Schedule(() => Child = control = new FriendsOnlineStatusControl + public void SetUp() => Schedule(() => Child = control = new FriendOnlineStreamControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -55,9 +55,9 @@ namespace osu.Game.Tests.Visual.UserInterface } })); - AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Count == 3); - AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Count == 1); - AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Count == 2); + AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.All)?.Count == 3); + AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Online)?.Count == 1); + AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Offline)?.Count == 2); } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs similarity index 94% rename from osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 3514bf7ff7..3c9b31daae 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsLayout : CompositeDrawable + public class FriendDisplay : CompositeDrawable { private List users = new List(); @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { users = value; - onlineStatusControl.Populate(value); + onlineStreamControl.Populate(value); } } @@ -39,14 +39,14 @@ namespace osu.Game.Overlays.Dashboard.Friends private Drawable currentContent; - private readonly FriendsOnlineStatusControl onlineStatusControl; + private readonly FriendOnlineStreamControl onlineStreamControl; private readonly Box background; private readonly Box controlBackground; private readonly UserListToolbar userListToolbar; private readonly Container itemsPlaceholder; private readonly LoadingLayer loading; - public FriendsLayout() + public FriendDisplay() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Top = 20, Horizontal = 45 }, - Child = onlineStatusControl = new FriendsOnlineStatusControl(), + Child = onlineStreamControl = new FriendOnlineStreamControl(), } } }, @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { base.LoadComplete(); - onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); + onlineStreamControl.Current.BindValueChanged(_ => recreatePanels()); userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); } @@ -179,16 +179,16 @@ namespace osu.Game.Overlays.Dashboard.Friends private List getUsersInCurrentGroup() { - switch (onlineStatusControl.Current.Value?.Status) + switch (onlineStreamControl.Current.Value?.Status) { default: - case FriendsOnlineStatus.All: + case OnlineStatus.All: return users; - case FriendsOnlineStatus.Offline: + case OnlineStatus.Offline: return users.Where(u => !u.IsOnline).ToList(); - case FriendsOnlineStatus.Online: + case OnlineStatus.Online: return users.Where(u => u.IsOnline).ToList(); } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs similarity index 53% rename from osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs index c54e9e2a06..28546ceab8 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs @@ -7,9 +7,9 @@ using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsOnlineStatusControl : OverlayStreamControl + public class FriendOnlineStreamControl : OverlayStreamControl { - protected override OverlayStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); + protected override OverlayStreamItem CreateStreamItem(FriendStream value) => new FriendsOnlineStatusItem(value); public void Populate(List users) { @@ -18,9 +18,9 @@ namespace osu.Game.Overlays.Dashboard.Friends var userCount = users.Count; var onlineUsersCount = users.Count(user => user.IsOnline); - AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount)); + AddItem(new FriendStream(OnlineStatus.All, userCount)); + AddItem(new FriendStream(OnlineStatus.Online, onlineUsersCount)); + AddItem(new FriendStream(OnlineStatus.Offline, userCount - onlineUsersCount)); Current.Value = Items.FirstOrDefault(); } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs similarity index 57% rename from osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendStream.cs index d5fad1ffd3..4abece9a8d 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs @@ -3,23 +3,16 @@ namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsBundle + public class FriendStream { - public FriendsOnlineStatus Status { get; } + public OnlineStatus Status { get; } public int Count { get; } - public FriendsBundle(FriendsOnlineStatus status, int count) + public FriendStream(OnlineStatus status, int count) { Status = status; Count = count; } } - - public enum FriendsOnlineStatus - { - All, - Online, - Offline - } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index eada9420ea..7e902203f8 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs @@ -7,9 +7,9 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsOnlineStatusItem : OverlayStreamItem + public class FriendsOnlineStatusItem : OverlayStreamItem { - public FriendsOnlineStatusItem(FriendsBundle value) + public FriendsOnlineStatusItem(FriendStream value) : base(value) { } @@ -22,13 +22,13 @@ namespace osu.Game.Overlays.Dashboard.Friends { switch (Value.Status) { - case FriendsOnlineStatus.All: + case OnlineStatus.All: return Color4.White; - case FriendsOnlineStatus.Online: + case OnlineStatus.Online: return colours.GreenLight; - case FriendsOnlineStatus.Offline: + case OnlineStatus.Offline: return Color4.Black; default: diff --git a/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs new file mode 100644 index 0000000000..6f2f55a6ed --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs @@ -0,0 +1,12 @@ +// 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.Overlays.Dashboard.Friends +{ + public enum OnlineStatus + { + All, + Online, + Offline + } +} From bf70276496b072dad7009cb4b1c27848b7710edd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Mar 2020 11:12:36 +0900 Subject: [PATCH 381/387] Fix test re-using the same beatmap sets --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 5b4c57e28f..6b4090ea58 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -234,6 +234,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Create beatmaps for taiko only", () => { + sets.Clear(); + var rulesetBeatmapSet = createTestBeatmapSet(1); var taikoRuleset = rulesets.AvailableRulesets.ElementAt(1); rulesetBeatmapSet.Beatmaps.ForEach(b => From 27ae2d29aac6d10f13a696713d5a9de78b0056ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 11:47:24 +0900 Subject: [PATCH 382/387] Add ability to adjust (and save) chroma-key area width --- osu.Game.Tournament/Models/LadderInfo.cs | 8 +++++++- .../Screens/Gameplay/GameplayScreen.cs | 14 ++++++++++++-- osu.Game.Tournament/Screens/SetupScreen.cs | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 5db0b01547..c2e6da9ca5 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -24,7 +24,13 @@ namespace osu.Game.Tournament.Models // only used for serialisation public List Progressions = new List(); - [JsonIgnore] + [JsonIgnore] // updated manually in TournamentGameBase public Bindable CurrentMatch = new Bindable(); + + public Bindable ChromaKeyWidth = new BindableInt(1024) + { + MinValue = 640, + MaxValue = 1366, + }; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 8920990d1b..64a5cd6dec 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -35,6 +36,8 @@ namespace osu.Game.Tournament.Screens.Gameplay [Resolved] private TournamentMatchChatDisplay chat { get; set; } + private Box chroma; + [BackgroundDependencyLoader] private void load(LadderInfo ladder, MatchIPCInfo ipc, Storage storage) { @@ -60,11 +63,10 @@ namespace osu.Game.Tournament.Screens.Gameplay Origin = Anchor.TopCentre, Children = new Drawable[] { - new Box + chroma = new Box { // chroma key area for stable gameplay Name = "chroma", - RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Height = 512, @@ -93,6 +95,12 @@ namespace osu.Game.Tournament.Screens.Gameplay RelativeSizeAxes = Axes.X, Text = "Toggle chat", Action = () => { State.Value = State.Value == TourneyState.Idle ? TourneyState.Playing : TourneyState.Idle; } + }, + new SettingsSlider + { + LabelText = "Chroma Width", + Bindable = LadderInfo.ChromaKeyWidth, + KeyboardStep = 1, } } } @@ -101,6 +109,8 @@ namespace osu.Game.Tournament.Screens.Gameplay State.BindTo(ipc.State); State.BindValueChanged(stateChanged, true); + ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); + currentMatch.BindValueChanged(m => { warmup.Value = m.NewValue.Team1Score.Value + m.NewValue.Team2Score.Value == 0; diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index b7f8b2bfd6..c91379b2d6 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tournament.Screens { windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080); } - } + }, }; } From 1b6342438f8cc54ff14dad811d54512c7e8af29b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 12:03:33 +0900 Subject: [PATCH 383/387] Hide scrollbars in tournament chat display --- .../Components/TournamentMatchChatDisplay.cs | 11 +++++++++++ osu.Game/Online/Chat/StandAloneChatDisplay.cs | 17 ++++++++++------- osu.Game/Overlays/Chat/DrawableChannel.cs | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 2a183d0d45..fe22d1e76d 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -70,6 +70,17 @@ namespace osu.Game.Tournament.Components protected override ChatLine CreateMessage(Message message) => new MatchMessage(message); + protected override StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => new MatchChannel(channel); + + public class MatchChannel : StandAloneDrawableChannel + { + public MatchChannel(Channel channel) + : base(channel) + { + ScrollbarVisible = false; + } + } + protected class MatchMessage : StandAloneMessage { public MatchMessage(Message message) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 0914f688e9..4fbeac1db9 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Online.Chat protected ChannelManager ChannelManager; - private DrawableChannel drawableChannel; + private StandAloneDrawableChannel drawableChannel; private readonly bool postingTextbox; @@ -77,6 +77,9 @@ namespace osu.Game.Online.Chat ChannelManager = manager; } + protected virtual StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => + new StandAloneDrawableChannel(channel); + private void postMessage(TextBox sender, bool newtext) { var text = textbox.Text.Trim(); @@ -100,14 +103,14 @@ namespace osu.Game.Online.Chat if (e.NewValue == null) return; - AddInternal(drawableChannel = new StandAloneDrawableChannel(e.NewValue) - { - CreateChatLineAction = CreateMessage, - Padding = new MarginPadding { Bottom = postingTextbox ? textbox_height : 0 } - }); + drawableChannel = CreateDrawableChannel(e.NewValue); + drawableChannel.CreateChatLineAction = CreateMessage; + drawableChannel.Padding = new MarginPadding { Bottom = postingTextbox ? textbox_height : 0 }; + + AddInternal(drawableChannel); } - protected class StandAloneDrawableChannel : DrawableChannel + public class StandAloneDrawableChannel : DrawableChannel { public Func CreateChatLineAction; diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 443f2b7bf7..6019657cf0 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -26,6 +26,20 @@ namespace osu.Game.Overlays.Chat protected FillFlowContainer ChatLineFlow; private OsuScrollContainer scroll; + private bool scrollbarVisible = true; + + public bool ScrollbarVisible + { + set + { + if (scrollbarVisible == value) return; + + scrollbarVisible = value; + if (scroll != null) + scroll.ScrollbarVisible = value; + } + } + [Resolved] private OsuColour colours { get; set; } @@ -44,6 +58,7 @@ namespace osu.Game.Overlays.Chat Masking = true, Child = scroll = new OsuScrollContainer { + ScrollbarVisible = scrollbarVisible, RelativeSizeAxes = Axes.Both, // Some chat lines have effects that slightly protrude to the bottom, // which we do not want to mask away, hence the padding. From 5106d275ca1cf58f26897f1f962762fa8d6119c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Mar 2020 12:08:15 +0900 Subject: [PATCH 384/387] Remove CentreHit/RimHit hitobject abstraction --- .../Mods/TestSceneTaikoModPerfect.cs | 2 +- .../TaikoBeatmapConversionTest.cs | 4 +- .../TestSceneTaikoPlayfield.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 46 +++++-------------- .../Preprocessing/TaikoDifficultyHitObject.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/CentreHit.cs | 9 ---- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 4 ++ osu.Game.Rulesets.Taiko/Objects/HitType.cs | 21 +++++++++ osu.Game.Rulesets.Taiko/Objects/RimHit.cs | 9 ---- .../Replays/TaikoAutoGenerator.cs | 2 +- .../UI/DrawableTaikoRuleset.cs | 10 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 4 +- 12 files changed, 50 insertions(+), 65 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/CentreHit.cs create mode 100644 osu.Game.Rulesets.Taiko/Objects/HitType.cs delete mode 100644 osu.Game.Rulesets.Taiko/Objects/RimHit.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index d3be2cdf0d..26c90ad295 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new CentreHit { StartTime = 1000 }), shouldMiss); + public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Hit { StartTime = 1000, Type = HitType.Centre }), shouldMiss); [TestCase(false)] [TestCase(true)] diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index f23fd6d3f9..8c26ca70ac 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -27,8 +27,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { StartTime = hitObject.StartTime, EndTime = hitObject.GetEndTime(), - IsRim = hitObject is RimHit, - IsCentre = hitObject is CentreHit, + IsRim = (hitObject as Hit)?.Type == HitType.Rim, + IsCentre = (hitObject as Hit)?.Type == HitType.Centre, IsDrumRoll = hitObject is DrumRoll, IsSwell = hitObject is Swell, IsStrong = ((TaikoHitObject)hitObject).IsStrong diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs index c01eef5252..0d9e813c60 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Tests WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap { - HitObjects = new List { new CentreHit() }, + HitObjects = new List { new Hit { Type = HitType.Centre } }, BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(), diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index cc9d6e4470..695ada3a00 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -124,24 +124,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE); strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH); - if (isRim) + yield return new Hit { - yield return new RimHit - { - StartTime = j, - Samples = currentSamples, - IsStrong = strong - }; - } - else - { - yield return new CentreHit - { - StartTime = j, - Samples = currentSamples, - IsStrong = strong - }; - } + StartTime = j, + Type = isRim ? HitType.Rim : HitType.Centre, + Samples = currentSamples, + IsStrong = strong + }; i = (i + 1) % allSamples.Count; } @@ -180,24 +169,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { bool isRim = samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE); - if (isRim) + yield return new Hit { - yield return new RimHit - { - StartTime = obj.StartTime, - Samples = obj.Samples, - IsStrong = strong - }; - } - else - { - yield return new CentreHit - { - StartTime = obj.StartTime, - Samples = obj.Samples, - IsStrong = strong - }; - } + StartTime = obj.StartTime, + Type = isRim ? HitType.Rim : HitType.Centre, + Samples = obj.Samples, + IsStrong = strong + }; break; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 24345275c1..6807142327 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate) : base(hitObject, lastObject, clockRate) { - HasTypeChange = lastObject is RimHit != hitObject is RimHit; + HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/CentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/CentreHit.cs deleted file mode 100644 index a6354b16ed..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/CentreHit.cs +++ /dev/null @@ -1,9 +0,0 @@ -// 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.Rulesets.Taiko.Objects -{ - public class CentreHit : Hit - { - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index 6cc9357580..2aca701515 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -5,5 +5,9 @@ namespace osu.Game.Rulesets.Taiko.Objects { public class Hit : TaikoHitObject { + /// + /// The that actuates this . + /// + public HitType Type { get; set; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/HitType.cs b/osu.Game.Rulesets.Taiko/Objects/HitType.cs new file mode 100644 index 0000000000..17b3fdbd04 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/HitType.cs @@ -0,0 +1,21 @@ +// 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.Rulesets.Taiko.Objects +{ + /// + /// The type of a . + /// + public enum HitType + { + /// + /// A that can be hit by the centre portion of the drum. + /// + Centre, + + /// + /// A that can be hit by the rim portion of the drum. + /// + Rim + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/RimHit.cs b/osu.Game.Rulesets.Taiko/Objects/RimHit.cs deleted file mode 100644 index 6f6b089e03..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/RimHit.cs +++ /dev/null @@ -1,9 +0,0 @@ -// 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.Rulesets.Taiko.Objects -{ - public class RimHit : Hit - { - } -} diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 48eb33976e..273f4e4105 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Replays { TaikoAction[] actions; - if (hit is CentreHit) + if (hit.Type == HitType.Centre) { actions = h.IsStrong ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 0c7495aa52..9196bbf13e 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -48,11 +48,11 @@ namespace osu.Game.Rulesets.Taiko.UI { switch (h) { - case CentreHit centreHit: - return new DrawableCentreHit(centreHit); - - case RimHit rimHit: - return new DrawableRimHit(rimHit); + case Hit hit: + if (hit.Type == HitType.Centre) + return new DrawableCentreHit(hit); + else + return new DrawableRimHit(hit); case DrumRoll drumRoll: return new DrawableDrumRoll(drumRoll); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index a10f70a344..bde9085c23 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -14,9 +14,9 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; using osuTK; using osuTK.Graphics; @@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (!result.IsHit) break; - bool isRim = judgedObject.HitObject is RimHit; + bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim)); From 3a3df06e0b2e0a38ab2c1591d012a78859b164cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 13:11:40 +0900 Subject: [PATCH 385/387] Fix some pieces of SettingsItem getting dimmed twice when disabled --- .../Overlays/Settings/SettingsCheckbox.cs | 6 ++---- osu.Game/Overlays/Settings/SettingsItem.cs | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsCheckbox.cs b/osu.Game/Overlays/Settings/SettingsCheckbox.cs index a554159fd7..437b2e45b3 100644 --- a/osu.Game/Overlays/Settings/SettingsCheckbox.cs +++ b/osu.Game/Overlays/Settings/SettingsCheckbox.cs @@ -8,16 +8,14 @@ namespace osu.Game.Overlays.Settings { public class SettingsCheckbox : SettingsItem { - private OsuCheckbox checkbox; - private string labelText; - protected override Drawable CreateControl() => checkbox = new OsuCheckbox(); + protected override Drawable CreateControl() => new OsuCheckbox(); public override string LabelText { get => labelText; - set => checkbox.LabelText = labelText = value; + set => ((OsuCheckbox)Control).LabelText = labelText = value; } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index e89f2adf0b..c2dd40d2a6 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -33,22 +33,24 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; - private SpriteText text; + private SpriteText labelText; public bool ShowsDefaultIndicator = true; public virtual string LabelText { - get => text?.Text ?? string.Empty; + get => labelText?.Text ?? string.Empty; set { - if (text == null) + if (labelText == null) { // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, text = new OsuSpriteText()); + FlowContent.Insert(-1, labelText = new OsuSpriteText()); + + updateDisabled(); } - text.Text = value; + labelText.Text = value; } } @@ -96,13 +98,19 @@ namespace osu.Game.Overlays.Settings if (controlWithCurrent != null) { controlWithCurrent.Current.ValueChanged += _ => SettingChanged?.Invoke(); - controlWithCurrent.Current.DisabledChanged += disabled => { Colour = disabled ? Color4.Gray : Color4.White; }; + controlWithCurrent.Current.DisabledChanged += _ => updateDisabled(); if (ShowsDefaultIndicator) restoreDefaultButton.Bindable = controlWithCurrent.Current; } } + private void updateDisabled() + { + if (labelText != null) + labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; + } + private class RestoreDefaultValueButton : Container, IHasTooltip { private Bindable bindable; From 98e6896e934d5aa0f88a3977c7edc0a6e8005d92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 15:37:34 +0900 Subject: [PATCH 386/387] Rename test class --- .../{TestSceneFriendsLayout.cs => TestSceneFriendDisplay.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Online/{TestSceneFriendsLayout.cs => TestSceneFriendDisplay.cs} (97%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs similarity index 97% rename from osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs rename to osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index c0a617fe57..cf365a7614 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -13,7 +13,7 @@ using NUnit.Framework; namespace osu.Game.Tests.Visual.Online { - public class TestSceneFriendsLayout : OsuTestScene + public class TestSceneFriendDisplay : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { From 66f2a52dd286693aa0aecd6b3e0f343acef23f12 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2020 08:59:47 +0000 Subject: [PATCH 387/387] Bump Sentry from 2.1.0 to 2.1.1 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.0...2.1.1) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 46d17bcf05..3894c06994 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - +