diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 93fd3935c6..2eeb112450 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -15,12 +15,16 @@ using Microsoft.Win32; using osu.Desktop.Updater; using osu.Framework; using osu.Framework.Platform.Windows; +using osu.Framework.Screens; +using osu.Game.Screens; +using osu.Game.Screens.Menu; namespace osu.Desktop { internal class OsuGameDesktop : OsuGame { private readonly bool noVersionOverlay; + private VersionManager versionManager; public OsuGameDesktop(string[] args = null) : base(args) @@ -46,7 +50,7 @@ namespace osu.Desktop if (!noVersionOverlay) { - LoadComponentAsync(new VersionManager { Depth = int.MinValue }, v => + LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, v => { Add(v); v.State = Visibility.Visible; @@ -59,6 +63,23 @@ namespace osu.Desktop } } + protected override void ScreenChanged(OsuScreen current, Screen newScreen) + { + base.ScreenChanged(current, newScreen); + switch (newScreen) + { + case Intro _: + case MainMenu _: + if (versionManager != null) + versionManager.State = Visibility.Visible; + break; + default: + if (versionManager != null) + versionManager.State = Visibility.Hidden; + break; + } + } + public override void SetHost(GameHost host) { base.SetHost(host); diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs index f31bba1e1e..c1fd34d009 100644 --- a/osu.Desktop/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -128,11 +128,12 @@ namespace osu.Desktop.Overlays protected override void PopIn() { - this.FadeIn(1000); + this.FadeIn(1400, Easing.OutQuint); } protected override void PopOut() { + this.FadeOut(500, Easing.OutQuint); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs new file mode 100644 index 0000000000..1cf2694a90 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Catch.Objects +{ + public class CatchHitWindows : HitWindows + { + public override bool IsHitResultAllowed(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + case HitResult.Miss: + return true; + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 778d972b52..57f4355d6a 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -41,5 +42,7 @@ namespace osu.Game.Rulesets.Catch.Scoring Health.Value += Math.Max(result.Judgement.HealthIncreaseFor(result) - hpDrainRate, 0) * harshness; } + + protected override HitWindows CreateHitWindows() => new CatchHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 12b32c46ee..20a665c314 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -5,6 +5,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -157,5 +158,7 @@ namespace osu.Game.Rulesets.Mania.Scoring } } } + + protected override HitWindows CreateHitWindows() => new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 5e66d43c98..16f0af9875 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -123,8 +123,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(h => h is OsuModFlashlight)) { - // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. - aimValue *= 1.45f * lengthBonus; + // Apply object-based bonus for flashlight. + aimValue *= 1.0f + 0.35f * Math.Min(1.0f, totalHits / 200.0f) + + (totalHits > 200 ? 0.3f * Math.Min(1.0f, (totalHits - 200) / 300.0f) + + (totalHits > 500 ? (totalHits - 500) / 1200.0f : 0.0f) : 0.0f); } // Scale the aim value with accuracy _slightly_ diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index d508ec2636..2af1de7355 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -7,6 +7,7 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; +using osu.Framework.Caching; using osu.Framework.Configuration; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -26,8 +27,11 @@ namespace osu.Game.Rulesets.Osu.Objects public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; public double Duration => EndTime - StartTime; + private Cached endPositionCache; + + public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1); + public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t); - public override Vector2 EndPosition => Position + this.CurvePositionAt(1); public override int ComboIndex { @@ -56,7 +60,11 @@ namespace osu.Game.Rulesets.Osu.Objects public SliderPath Path { get => PathBindable.Value; - set => PathBindable.Value = value; + set + { + PathBindable.Value = value; + endPositionCache.Invalidate(); + } } public double Distance => Path.Distance; @@ -73,6 +81,8 @@ namespace osu.Game.Rulesets.Osu.Objects if (TailCircle != null) TailCircle.Position = EndPosition; + + endPositionCache.Invalidate(); } } @@ -92,7 +102,17 @@ namespace osu.Game.Rulesets.Osu.Objects public List> NodeSamples { get; set; } = new List>(); - public int RepeatCount { get; set; } + private int repeatCount; + + public int RepeatCount + { + get => repeatCount; + set + { + repeatCount = value; + endPositionCache.Invalidate(); + } + } /// /// The length of one span of this . @@ -169,7 +189,11 @@ namespace osu.Game.Rulesets.Osu.Objects private void createTicks() { - var length = Path.Distance; + // A very lenient maximum length of a slider for ticks to be generated. + // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. + const double max_length = 100000; + + var length = Math.Min(max_length, Path.Distance); var tickDistance = MathHelper.Clamp(TickDistance, 0, length); if (tickDistance == 0) return; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 9b3da1b8a8..a24efe4a1e 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -5,11 +5,11 @@ using System.Collections.Generic; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -22,7 +22,6 @@ namespace osu.Game.Rulesets.Osu.Scoring private float hpDrainRate; - private readonly Dictionary scoreResultCounts = new Dictionary(); private readonly Dictionary comboResultCounts = new Dictionary(); protected override void ApplyBeatmap(Beatmap beatmap) @@ -35,21 +34,9 @@ namespace osu.Game.Rulesets.Osu.Scoring protected override void Reset(bool storeResults) { base.Reset(storeResults); - - scoreResultCounts.Clear(); comboResultCounts.Clear(); } - public override void PopulateScore(ScoreInfo score) - { - base.PopulateScore(score); - - score.Statistics[HitResult.Great] = scoreResultCounts.GetOrDefault(HitResult.Great); - score.Statistics[HitResult.Good] = scoreResultCounts.GetOrDefault(HitResult.Good); - score.Statistics[HitResult.Meh] = scoreResultCounts.GetOrDefault(HitResult.Meh); - score.Statistics[HitResult.Miss] = scoreResultCounts.GetOrDefault(HitResult.Miss); - } - private const double harshness = 0.01; protected override void ApplyResult(JudgementResult result) @@ -59,10 +46,7 @@ namespace osu.Game.Rulesets.Osu.Scoring var osuResult = (OsuJudgementResult)result; if (result.Type != HitResult.None) - { - scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; comboResultCounts[osuResult.ComboType] = comboResultCounts.GetOrDefault(osuResult.ComboType) + 1; - } switch (result.Type) { @@ -89,5 +73,7 @@ namespace osu.Game.Rulesets.Osu.Scoring } protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(judgement); + + protected override HitWindows CreateHitWindows() => new OsuHitWindows(); } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 318efdbf3e..87481c800d 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -3,6 +3,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; @@ -65,5 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring Health.Value = 0; } + + protected override HitWindows CreateHitWindows() => new TaikoHitWindows(); } } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 26167cb24a..c8fd531fcc 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - loadOszIntoOsu(loadOsu(host)); + LoadOszIntoOsu(loadOsu(host)); } finally { @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var imported = loadOszIntoOsu(osu); + var imported = LoadOszIntoOsu(osu); deleteBeatmapSet(imported, osu); } @@ -69,8 +69,8 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var imported = loadOszIntoOsu(osu); - var importedSecondTime = loadOszIntoOsu(osu); + var imported = LoadOszIntoOsu(osu); + var importedSecondTime = LoadOszIntoOsu(osu); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Beatmaps.IO manager.ItemAdded += (_, __, ___) => fireCount++; manager.ItemRemoved += _ => fireCount++; - var imported = loadOszIntoOsu(osu); + var imported = LoadOszIntoOsu(osu); Assert.AreEqual(0, fireCount -= 1); @@ -160,12 +160,12 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); var manager = osu.Dependencies.Get(); - var imported = loadOszIntoOsu(osu); + var imported = LoadOszIntoOsu(osu); imported.Hash += "-changed"; manager.Update(imported); - var importedSecondTime = loadOszIntoOsu(osu); + var importedSecondTime = LoadOszIntoOsu(osu); Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID); @@ -191,11 +191,11 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var imported = loadOszIntoOsu(osu); + var imported = LoadOszIntoOsu(osu); deleteBeatmapSet(imported, osu); - var importedSecondTime = loadOszIntoOsu(osu); + var importedSecondTime = LoadOszIntoOsu(osu); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); @@ -262,7 +262,7 @@ namespace osu.Game.Tests.Beatmaps.IO } } - private string createTemporaryBeatmap() + private static string createTemporaryBeatmap() { var temp = Path.GetTempFileName() + ".osz"; File.Copy(TEST_OSZ_PATH, temp, true); @@ -270,7 +270,7 @@ namespace osu.Game.Tests.Beatmaps.IO return temp; } - private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu, string path = null) + public static BeatmapSetInfo LoadOszIntoOsu(OsuGameBase osu, string path = null) { var temp = path ?? createTemporaryBeatmap(); @@ -305,7 +305,7 @@ namespace osu.Game.Tests.Beatmaps.IO return osu; } - private void ensureLoaded(OsuGameBase osu, int timeout = 60000) + private static void ensureLoaded(OsuGameBase osu, int timeout = 60000) { IEnumerable resultSets = null; var store = osu.Dependencies.Get(); @@ -343,7 +343,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(beatmap?.HitObjects.Any() == true); } - private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) + private static void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) { Task task = Task.Run(() => { diff --git a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs deleted file mode 100644 index 8fd8880fd6..0000000000 --- a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Components; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual -{ - [TestFixture] - public class TestCaseDrawableRoom : OsuTestCase - { - private RulesetStore rulesets; - - protected override void LoadComplete() - { - base.LoadComplete(); - - DrawableRoom first; - Add(new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - Width = 580f, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - first = new DrawableRoom(new Room - { - Name = { Value = @"Great Room Right Here" }, - Host = { Value = new User { Username = @"Naeferith", Id = 9492835, Country = new Country { FlagName = @"FR" } } }, - Status = { Value = new RoomStatusOpen() }, - Type = { Value = new GameTypeTeamVersus() }, - Beatmap = - { - Value = new BeatmapInfo - { - StarDifficulty = 4.65, - Ruleset = rulesets.GetRuleset(3), - Metadata = new BeatmapMetadata - { - Title = @"Critical Crystal", - Artist = @"Seiryu", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh//beatmaps/376340/covers/cover.jpg?1456478455", - }, - }, - }, - }, - }, - Participants = - { - Value = new[] - { - new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 1355 } } }, - new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 8756 } } }, - }, - }, - }), - new DrawableRoom(new Room - { - Name = { Value = @"Relax It's The Weekend" }, - Host = { Value = new User { Username = @"peppy", Id = 2, Country = new Country { FlagName = @"AU" } } }, - Status = { Value = new RoomStatusPlaying() }, - Type = { Value = new GameTypeTagTeam() }, - Beatmap = - { - Value = new BeatmapInfo - { - StarDifficulty = 1.96, - Ruleset = rulesets.GetRuleset(0), - Metadata = new BeatmapMetadata - { - Title = @"Serendipity", - Artist = @"ZAQ", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh//beatmaps/526839/covers/cover.jpg?1493815706", - }, - }, - }, - }, - }, - Participants = - { - Value = new[] - { - new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 578975 } } }, - new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24554 } } }, - }, - }, - }), - } - }); - - AddStep(@"select", () => first.State = SelectionState.Selected); - AddStep(@"change title", () => first.Room.Name.Value = @"I Changed Name"); - AddStep(@"change host", () => first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }); - AddStep(@"change status", () => first.Room.Status.Value = new RoomStatusPlaying()); - AddStep(@"change type", () => first.Room.Type.Value = new GameTypeVersus()); - AddStep(@"change beatmap", () => first.Room.Beatmap.Value = null); - AddStep(@"change participants", () => first.Room.Participants.Value = new[] - { - new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 1254 } } }, - new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 123189 } } }, - }); - AddStep(@"deselect", () => first.State = SelectionState.NotSelected); - } - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - this.rulesets = rulesets; - } - } -} diff --git a/osu.Game.Tests/Visual/TestCaseLounge.cs b/osu.Game.Tests/Visual/TestCaseLounge.cs deleted file mode 100644 index e0e6332ef0..0000000000 --- a/osu.Game.Tests/Visual/TestCaseLounge.cs +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; -using osu.Game.Screens; -using osu.Game.Screens.Backgrounds; -using osu.Game.Screens.Multi.Components; -using osu.Game.Screens.Multi.Screens.Lounge; -using osu.Game.Users; -using osuTK.Input; - -namespace osu.Game.Tests.Visual -{ - [TestFixture] - public class TestCaseLounge : ManualInputManagerTestCase - { - private TestLounge lounge; - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - lounge = new TestLounge(); - - Room[] rooms = - { - new Room - { - Name = { Value = @"Just Another Room" }, - Host = { Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } } }, - Status = { Value = new RoomStatusPlaying() }, - Availability = { Value = RoomAvailability.Public }, - Type = { Value = new GameTypeTagTeam() }, - Beatmap = - { - Value = new BeatmapInfo - { - StarDifficulty = 5.65, - Ruleset = rulesets.GetRuleset(0), - Metadata = new BeatmapMetadata - { - Title = @"Sidetracked Day (Short Ver.)", - Artist = @"VINXIS", - AuthorString = @"Hobbes2", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/767600/covers/cover.jpg?1526243446", - }, - }, - }, - } - }, - MaxParticipants = { Value = 10 }, - Participants = - { - Value = new[] - { - new User { Username = @"flyte", Id = 3103765, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 142 } } }, - new User { Username = @"Cookiezi", Id = 124493, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 546 } } }, - new User { Username = @"Angelsim", Id = 1777162, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 287 } } }, - new User { Username = @"Rafis", Id = 2558286, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 468 } } }, - new User { Username = @"hvick225", Id = 50265, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 325 } } }, - new User { Username = @"peppy", Id = 2, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 625 } } }, - } - } - }, - new Room - { - Name = { Value = @"Not Just Any Room" }, - Host = { Value = new User { Username = @"Monstrata", Id = 2706438, Country = new Country { FlagName = @"CA" } } }, - Status = { Value = new RoomStatusOpen() }, - Availability = { Value = RoomAvailability.FriendsOnly }, - Type = { Value = new GameTypeTeamVersus() }, - Beatmap = - { - Value = new BeatmapInfo - { - StarDifficulty = 2.73, - Ruleset = rulesets.GetRuleset(0), - Metadata = new BeatmapMetadata - { - Title = @"lit(var)", - Artist = @"kensuke ushio", - AuthorString = @"Monstrata", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/623972/covers/cover.jpg?1521167183", - }, - }, - }, - } - }, - Participants = - { - Value = new[] - { - new User { Username = @"Jeby", Id = 3136279, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 3497 } } }, - new User { Username = @"DualAkira", Id = 5220933, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 643 } } }, - new User { Username = @"Datenshi Yohane", Id = 7171857, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 10555 } } }, - } - } - }, - new Room - { - Name = { Value = @"room THE FINAL" }, - Host = { Value = new User { Username = @"Delis", Id = 1603923, Country = new Country { FlagName = @"JP" } } }, - Status = { Value = new RoomStatusPlaying() }, - Availability = { Value = RoomAvailability.Public }, - Type = { Value = new GameTypeTagTeam() }, - Beatmap = - { - Value = new BeatmapInfo - { - StarDifficulty = 4.48, - Ruleset = rulesets.GetRuleset(3), - Metadata = new BeatmapMetadata - { - Title = @"ONIGIRI FREEWAY", - Artist = @"OISHII", - AuthorString = @"Mentholzzz", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/663098/covers/cover.jpg?1521898837", - }, - }, - }, - } - }, - MaxParticipants = { Value = 30 }, - Participants = - { - Value = new[] - { - new User { Username = @"KizuA", Id = 6510442, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 5372 } } }, - new User { Username = @"Colored", Id = 827563, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 810 } } }, - new User { Username = @"Beryl", Id = 3817591, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 10096 } } }, - } - } - }, - }; - - AddStep(@"show", () => Add(lounge)); - AddStep(@"set rooms", () => lounge.Rooms = rooms); - selectAssert(0); - AddStep(@"clear rooms", () => lounge.Rooms = new Room[] {}); - AddAssert(@"no room selected", () => lounge.SelectedRoom == null); - AddStep(@"set rooms", () => lounge.Rooms = rooms); - selectAssert(1); - AddStep(@"open room 1", () => clickRoom(1)); - AddUntilStep(() => lounge.ChildScreen?.IsCurrentScreen == true, "wait until room current"); - AddStep(@"make lounge current", lounge.MakeCurrent); - filterAssert(@"THE FINAL", LoungeTab.Public, 1); - filterAssert(string.Empty, LoungeTab.Public, 2); - filterAssert(string.Empty, LoungeTab.Private, 1); - filterAssert(string.Empty, LoungeTab.Public, 2); - filterAssert(@"no matches", LoungeTab.Public, 0); - AddStep(@"clear rooms", () => lounge.Rooms = new Room[] {}); - AddStep(@"set rooms", () => lounge.Rooms = rooms); - AddAssert(@"no matches after clear", () => !lounge.ChildRooms.Any()); - filterAssert(string.Empty, LoungeTab.Public, 2); - AddStep(@"exit", lounge.Exit); - } - - private void clickRoom(int n) - { - InputManager.MoveMouseTo(lounge.ChildRooms.ElementAt(n)); - InputManager.Click(MouseButton.Left); - } - - private void selectAssert(int n) - { - AddStep($@"select room {n}", () => clickRoom(n)); - AddAssert($@"room {n} selected", () => lounge.SelectedRoom == lounge.ChildRooms.ElementAt(n).Room); - } - - private void filterAssert(string filter, LoungeTab tab, int endCount) - { - AddStep($@"filter '{filter}', {tab}", () => lounge.SetFilter(filter, tab)); - AddAssert(@"filtered correctly", () => lounge.ChildRooms.Count() == endCount); - } - - private class TestLounge : Lounge - { - protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); - - public IEnumerable ChildRooms => RoomsContainer.Children.Where(r => r.MatchingFilter); - public Room SelectedRoom => Inspector.Room; - - public void SetFilter(string filter, LoungeTab tab) - { - Filter.Search.Current.Value = filter; - Filter.Tabs.Current.Value = tab; - } - } - } -} diff --git a/osu.Game.Tests/Visual/TestCaseLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/TestCaseLoungeRoomsContainer.cs new file mode 100644 index 0000000000..3e9f2fb3a4 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseLoungeRoomsContainer.cs @@ -0,0 +1,101 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi; +using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Users; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseLoungeRoomsContainer : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(RoomsContainer), + typeof(DrawableRoom) + }; + + [Cached(Type = typeof(IRoomManager))] + private TestRoomManager roomManager = new TestRoomManager(); + + public TestCaseLoungeRoomsContainer() + { + RoomsContainer container; + + Child = container = new RoomsContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + JoinRequested = joinRequested + }; + + AddStep("clear rooms", () => roomManager.Rooms.Clear()); + + AddStep("add rooms", () => + { + for (int i = 0; i < 3; i++) + { + roomManager.Rooms.Add(new Room + { + RoomID = { Value = i }, + Name = { Value = $"Room {i}" }, + Host = { Value = new User { Username = "Host" } }, + EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) } + }); + } + }); + + AddAssert("has 2 rooms", () => container.Rooms.Count == 3); + AddStep("remove first room", () => roomManager.Rooms.Remove(roomManager.Rooms.FirstOrDefault())); + AddAssert("has 2 rooms", () => container.Rooms.Count == 2); + AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); + + AddStep("select first room", () => container.Rooms.First().Action?.Invoke()); + AddAssert("first room selected", () => container.SelectedRoom.Value == roomManager.Rooms.First()); + + AddStep("join first room", () => container.Rooms.First().Action?.Invoke()); + AddAssert("first room joined", () => roomManager.Rooms.First().Status.Value is JoinedRoomStatus); + } + + private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus(); + + private class TestRoomManager : IRoomManager + { + public event Action RoomsUpdated; + + public readonly BindableCollection Rooms = new BindableCollection(); + IBindableCollection IRoomManager.Rooms => Rooms; + + public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room); + + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + { + } + + public void PartRoom() + { + } + + public void Filter(FilterCriteria criteria) + { + } + } + + private class JoinedRoomStatus : RoomStatus + { + public override string Message => "Joined"; + + public override Color4 GetAppropriateColour(OsuColour colours) => colours.Yellow; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMatch.cs b/osu.Game.Tests/Visual/TestCaseMatch.cs deleted file mode 100644 index bb22358425..0000000000 --- a/osu.Game.Tests/Visual/TestCaseMatch.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Screens.Match; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual -{ - [TestFixture] - public class TestCaseMatch : OsuTestCase - { - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - Room room = new Room - { - Name = { Value = @"One Awesome Room" }, - Status = { Value = new RoomStatusOpen() }, - Availability = { Value = RoomAvailability.Public }, - Type = { Value = new GameTypeTeamVersus() }, - Beatmap = - { - Value = new BeatmapInfo - { - StarDifficulty = 5.02, - Ruleset = rulesets.GetRuleset(1), - Metadata = new BeatmapMetadata - { - Title = @"Paradigm Shift", - Artist = @"Morimori Atsushi", - AuthorString = @"eiri-", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/765055/covers/cover.jpg?1526955337", - }, - }, - }, - }, - }, - MaxParticipants = { Value = 5 }, - Participants = - { - Value = new[] - { - new User - { - Username = @"eiri-", - Id = 3388410, - Country = new Country { FlagName = @"US" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/3388410/00a8486a247831e1cc4375db519f611ac970bda8bc0057d78b0f540ea38c3e58.jpeg", - IsSupporter = true, - }, - new User - { - Username = @"Nepuri", - Id = 6637817, - Country = new Country { FlagName = @"DE" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/6637817/9085fc60248b6b5327a72c1dcdecf2dbedba810ae0ab6bcf7224e46b1339632a.jpeg", - IsSupporter = true, - }, - new User - { - Username = @"goheegy", - Id = 8057655, - Country = new Country { FlagName = @"GB" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8057655/21cec27c25a11dc197a4ec6a74253dbabb495949b0e0697113352f12007018c5.jpeg", - }, - new User - { - Username = @"Alumetri", - Id = 5371497, - Country = new Country { FlagName = @"RU" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/5371497/e023b8c7fbe3613e64bd4856703517ea50fbed8a5805dc9acda9efe9897c67e2.jpeg", - }, - } - }, - }; - - Match match = new Match(room); - - AddStep(@"show", () => Add(match)); - AddStep(@"null beatmap", () => room.Beatmap.Value = null); - AddStep(@"change name", () => room.Name.Value = @"Two Awesome Rooms"); - AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying()); - AddStep(@"change availability", () => room.Availability.Value = RoomAvailability.FriendsOnly); - AddStep(@"change type", () => room.Type.Value = new GameTypeTag()); - AddStep(@"change beatmap", () => room.Beatmap.Value = new BeatmapInfo - { - StarDifficulty = 4.33, - Ruleset = rulesets.GetRuleset(2), - Metadata = new BeatmapMetadata - { - Title = @"Yasashisa no Riyuu", - Artist = @"ChouCho", - AuthorString = @"celerih", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/685391/covers/cover.jpg?1524597970", - }, - }, - }, - }); - - AddStep(@"null max participants", () => room.MaxParticipants.Value = null); - AddStep(@"change participants", () => room.Participants.Value = new[] - { - new User - { - Username = @"Spectator", - Id = 702598, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/702598/3bbf4cb8b8d2cf8b03145000a975ff27e191ab99b0920832e7dd67386280e288.jpeg", - IsSupporter = true, - }, - new User - { - Username = @"celerih", - Id = 4696296, - Country = new Country { FlagName = @"CA" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/4696296/7f8500731d0ac66d5472569d146a7be07d9460273361913f22c038867baddaef.jpeg", - }, - }); - - AddStep(@"exit", match.Exit); - } - } -} diff --git a/osu.Game.Tests/Visual/TestCaseMatchHeader.cs b/osu.Game.Tests/Visual/TestCaseMatchHeader.cs index 34f98f97c2..4c8cdbfd19 100644 --- a/osu.Game.Tests/Visual/TestCaseMatchHeader.cs +++ b/osu.Game.Tests/Visual/TestCaseMatchHeader.cs @@ -1,43 +1,54 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using NUnit.Framework; +using System; +using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Screens.Multi.Screens.Match; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.GameTypes; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Multi.Match.Components; namespace osu.Game.Tests.Visual { - [TestFixture] public class TestCaseMatchHeader : OsuTestCase { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(Header) + }; + public TestCaseMatchHeader() { - Header header = new Header(); - Add(header); + var room = new Room(); - AddStep(@"set beatmap set", () => header.BeatmapSet = new BeatmapSetInfo + var header = new Header(room); + + room.Playlist.Add(new PlaylistItem { - OnlineInfo = new BeatmapSetOnlineInfo + Beatmap = new BeatmapInfo { - Covers = new BeatmapSetOnlineCovers + Metadata = new BeatmapMetadata { - Cover = @"https://assets.ppy.sh/beatmaps/760757/covers/cover.jpg?1526944540", + Title = "Title", + Artist = "Artist", + AuthorString = "Author", }, + Version = "Version", + Ruleset = new OsuRuleset().RulesetInfo }, + RequiredMods = + { + new OsuModDoubleTime(), + new OsuModNoFail(), + new OsuModRelax(), + } }); - AddStep(@"change beatmap set", () => header.BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/761883/covers/cover.jpg?1525557400", - }, - }, - }); + room.Type.Value = new GameTypeTimeshift(); - AddStep(@"null beatmap set", () => header.BeatmapSet = null); + Child = header; } } } diff --git a/osu.Game.Tests/Visual/TestCaseMatchHostInfo.cs b/osu.Game.Tests/Visual/TestCaseMatchHostInfo.cs new file mode 100644 index 0000000000..8e733d388a --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMatchHostInfo.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseMatchHostInfo : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(HostInfo) + }; + + private readonly Bindable host = new Bindable(new User { Username = "SomeHost" }); + + public TestCaseMatchHostInfo() + { + HostInfo hostInfo; + + Child = hostInfo = new HostInfo + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + + hostInfo.Host.BindTo(host); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMatchInfo.cs b/osu.Game.Tests/Visual/TestCaseMatchInfo.cs index 205da6932f..4da500427b 100644 --- a/osu.Game.Tests/Visual/TestCaseMatchInfo.cs +++ b/osu.Game.Tests/Visual/TestCaseMatchInfo.cs @@ -1,56 +1,80 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Screens.Match; +using osu.Game.Screens.Multi.Match.Components; namespace osu.Game.Tests.Visual { [TestFixture] public class TestCaseMatchInfo : OsuTestCase { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(Info), + typeof(HeaderButton), + typeof(ReadyButton), + typeof(ViewBeatmapButton) + }; + [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - Info info = new Info(); + var room = new Room(); + + Info info = new Info(room); Add(info); - AddStep(@"set name", () => info.Name = @"Room Name?"); - AddStep(@"set availability", () => info.Availability = RoomAvailability.FriendsOnly); - AddStep(@"set status", () => info.Status = new RoomStatusPlaying()); - AddStep(@"set beatmap", () => info.Beatmap = new BeatmapInfo + AddStep(@"set name", () => room.Name.Value = @"Room Name?"); + AddStep(@"set availability", () => room.Availability.Value = RoomAvailability.FriendsOnly); + AddStep(@"set status", () => room.Status.Value = new RoomStatusPlaying()); + AddStep(@"set beatmap", () => { - StarDifficulty = 2.4, - Ruleset = rulesets.GetRuleset(0), - Metadata = new BeatmapMetadata + room.Playlist.Clear(); + room.Playlist.Add(new PlaylistItem { - Title = @"My Song", - Artist = @"VisualTests", - AuthorString = @"osu!lazer", - }, + Beatmap = new BeatmapInfo + { + StarDifficulty = 2.4, + Ruleset = rulesets.GetRuleset(0), + Metadata = new BeatmapMetadata + { + Title = @"My Song", + Artist = @"VisualTests", + AuthorString = @"osu!lazer", + }, + } + }); }); - AddStep(@"set type", () => info.Type = new GameTypeTagTeam()); - - AddStep(@"change name", () => info.Name = @"Room Name!"); - AddStep(@"change availability", () => info.Availability = RoomAvailability.InviteOnly); - AddStep(@"change status", () => info.Status = new RoomStatusOpen()); - AddStep(@"null beatmap", () => info.Beatmap = null); - AddStep(@"change type", () => info.Type = new GameTypeTeamVersus()); - AddStep(@"change beatmap", () => info.Beatmap = new BeatmapInfo + AddStep(@"change name", () => room.Name.Value = @"Room Name!"); + AddStep(@"change availability", () => room.Availability.Value = RoomAvailability.InviteOnly); + AddStep(@"change status", () => room.Status.Value = new RoomStatusOpen()); + AddStep(@"null beatmap", () => room.Playlist.Clear()); + AddStep(@"change beatmap", () => { - StarDifficulty = 4.2, - Ruleset = rulesets.GetRuleset(3), - Metadata = new BeatmapMetadata + room.Playlist.Clear(); + room.Playlist.Add(new PlaylistItem { - Title = @"Your Song", - Artist = @"Tester", - AuthorString = @"Someone", - }, + Beatmap = new BeatmapInfo + { + StarDifficulty = 4.2, + Ruleset = rulesets.GetRuleset(3), + Metadata = new BeatmapMetadata + { + Title = @"Your Song", + Artist = @"Tester", + AuthorString = @"Someone", + }, + } + }); }); } } diff --git a/osu.Game.Tests/Visual/TestCaseMatchLeaderboard.cs b/osu.Game.Tests/Visual/TestCaseMatchLeaderboard.cs new file mode 100644 index 0000000000..cf475de1f0 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMatchLeaderboard.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using Newtonsoft.Json; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseMatchLeaderboard : OsuTestCase + { + public TestCaseMatchLeaderboard() + { + Add(new MatchLeaderboard(new Room { RoomID = { Value = 3 } }) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + Scope = MatchLeaderboardScope.Overall, + }); + } + + [Resolved] + private APIAccess api { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + var req = new GetRoomScoresRequest(); + req.Success += v => { }; + req.Failure += _ => { }; + + api.Queue(req); + } + + private class GetRoomScoresRequest : APIRequest> + { + protected override string Target => "rooms/3/leaderboard"; + } + + private class RoomScore + { + [JsonProperty("user")] + public User User { get; set; } + + [JsonProperty("accuracy")] + public double Accuracy { get; set; } + + [JsonProperty("total_score")] + public int TotalScore { get; set; } + + [JsonProperty("pp")] + public double PP { get; set; } + + [JsonProperty("attempts")] + public int TotalAttempts { get; set; } + + [JsonProperty("completed")] + public int CompletedAttempts { get; set; } + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMatchParticipants.cs b/osu.Game.Tests/Visual/TestCaseMatchParticipants.cs index d6ae07252b..27aeb9b5eb 100644 --- a/osu.Game.Tests/Visual/TestCaseMatchParticipants.cs +++ b/osu.Game.Tests/Visual/TestCaseMatchParticipants.cs @@ -1,9 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using NUnit.Framework; +using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Game.Screens.Multi.Screens.Match; +using osu.Game.Screens.Multi.Match.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual @@ -11,16 +13,20 @@ namespace osu.Game.Tests.Visual [TestFixture] public class TestCaseMatchParticipants : OsuTestCase { + private readonly Bindable maxParticipants = new Bindable(); + private readonly Bindable> users = new Bindable>(); + public TestCaseMatchParticipants() { Participants participants; - Add(participants = new Participants - { - RelativeSizeAxes = Axes.Both, - }); - AddStep(@"set max to null", () => participants.Max = null); - AddStep(@"set users", () => participants.Users = new[] + Add(participants = new Participants { RelativeSizeAxes = Axes.Both }); + + participants.MaxParticipants.BindTo(maxParticipants); + participants.Users.BindTo(users); + + AddStep(@"set max to null", () => maxParticipants.Value = null); + AddStep(@"set users", () => users.Value = new[] { new User { @@ -48,9 +54,9 @@ namespace osu.Game.Tests.Visual }, }); - AddStep(@"set max", () => participants.Max = 10); - AddStep(@"clear users", () => participants.Users = new User[] { }); - AddStep(@"set max to null", () => participants.Max = null); + AddStep(@"set max", () => maxParticipants.Value = 10); + AddStep(@"clear users", () => users.Value = new User[] { }); + AddStep(@"set max to null", () => maxParticipants.Value = null); } } } diff --git a/osu.Game.Tests/Visual/TestCaseMatchResults.cs b/osu.Game.Tests/Visual/TestCaseMatchResults.cs new file mode 100644 index 0000000000..086f329608 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMatchResults.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +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.Online.Multiplayer; +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 +{ + public class TestCaseMatchResults : OsuTestCase + { + 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); + + Child = new TestMatchResults(new ScoreInfo + { + User = new User { Id = 10 }, + }); + } + + private class TestMatchResults : MatchResults + { + private readonly Room room; + + public TestMatchResults(ScoreInfo score) + : this(score, new Room + { + RoomID = { Value = 1 }, + Name = { Value = "an awesome room" } + }) + { + } + + public TestMatchResults(ScoreInfo score, Room room) + : base(score, room) + { + this.room = room; + } + + protected override IEnumerable CreateResultPages() => new[] { new TestRoomLeaderboardPageInfo(Score, Beatmap, room) }; + } + + private class TestRoomLeaderboardPageInfo : RoomLeaderboardPageInfo + { + private readonly ScoreInfo score; + private readonly WorkingBeatmap beatmap; + private readonly Room room; + + public TestRoomLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap, Room room) + : base(score, beatmap, room) + { + this.score = score; + this.beatmap = beatmap; + this.room = room; + } + + public override ResultsPage CreatePage() => new TestRoomLeaderboardPage(score, beatmap, room); + } + + private class TestRoomLeaderboardPage : RoomLeaderboardPage + { + public TestRoomLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap, Room room) + : base(score, beatmap, room) + { + } + + protected override MatchLeaderboard CreateLeaderboard(Room room) => new TestMatchLeaderboard(room); + } + + private class TestMatchLeaderboard : RoomLeaderboardPage.ResultsMatchLeaderboard + { + public TestMatchLeaderboard(Room room) + : base(room) + { + } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + var scores = Enumerable.Range(0, 50).Select(createRoomScore).ToArray(); + + scoresCallback?.Invoke(scores); + ScoresLoaded?.Invoke(scores); + + return null; + } + + private APIRoomScoreInfo createRoomScore(int id) => new APIRoomScoreInfo + { + User = new User { Id = id, Username = $"User {id}" }, + Accuracy = 0.98, + TotalScore = 987654, + TotalAttempts = 13, + CompletedBeatmaps = 5 + }; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/TestCaseMatchSettingsOverlay.cs new file mode 100644 index 0000000000..7fb9d4dded --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMatchSettingsOverlay.cs @@ -0,0 +1,161 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi; +using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.Match.Components; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseMatchSettingsOverlay : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(MatchSettingsOverlay) + }; + + [Cached(Type = typeof(IRoomManager))] + private TestRoomManager roomManager = new TestRoomManager(); + + private Room room; + private TestRoomSettings settings; + + [SetUp] + public void Setup() => Schedule(() => + { + room = new Room(); + settings = new TestRoomSettings(room) + { + RelativeSizeAxes = Axes.Both, + State = Visibility.Visible + }; + + Child = settings; + }); + + [Test] + public void TestButtonEnabledOnlyWithNameAndBeatmap() + { + AddStep("clear name and beatmap", () => + { + room.Name.Value = ""; + room.Playlist.Clear(); + }); + + AddAssert("button disabled", () => !settings.ApplyButton.Enabled); + + AddStep("set name", () => room.Name.Value = "Room name"); + AddAssert("button disabled", () => !settings.ApplyButton.Enabled); + + AddStep("set beatmap", () => room.Playlist.Add(new PlaylistItem { Beatmap = new DummyWorkingBeatmap().BeatmapInfo })); + AddAssert("button enabled", () => settings.ApplyButton.Enabled); + + AddStep("clear name", () => room.Name.Value = ""); + AddAssert("button disabled", () => !settings.ApplyButton.Enabled); + } + + [Test] + public void TestCorrectSettingsApplied() + { + const string expected_name = "expected name"; + TimeSpan expectedDuration = TimeSpan.FromMinutes(15); + + Room createdRoom = null; + + AddStep("setup", () => + { + settings.NameField.Current.Value = expected_name; + settings.DurationField.Current.Value = expectedDuration; + + roomManager.CreateRequested = r => + { + createdRoom = r; + return true; + }; + }); + + AddStep("create room", () => settings.ApplyButton.Action.Invoke()); + AddAssert("has correct name", () => createdRoom.Name.Value == expected_name); + AddAssert("has correct duration", () => createdRoom.Duration.Value == expectedDuration); + } + + [Test] + public void TestCreationFailureDisplaysError() + { + bool fail; + + AddStep("setup", () => + { + fail = true; + roomManager.CreateRequested = _ => !fail; + }); + AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); + + AddStep("create room", () => settings.ApplyButton.Action.Invoke()); + AddAssert("error displayed", () => settings.ErrorText.IsPresent); + AddAssert("error has correct text", () => settings.ErrorText.Text == TestRoomManager.FAILED_TEXT); + + AddStep("create room no fail", () => + { + fail = false; + settings.ApplyButton.Action.Invoke(); + }); + + AddUntilStep(() => !settings.ErrorText.IsPresent, "error not displayed"); + } + + private class TestRoomSettings : MatchSettingsOverlay + { + public new TriangleButton ApplyButton => base.ApplyButton; + + public new OsuTextBox NameField => base.NameField; + public new OsuDropdown DurationField => base.DurationField; + + public new OsuSpriteText ErrorText => base.ErrorText; + + public TestRoomSettings(Room room) + : base(room) + { + } + } + + private class TestRoomManager : IRoomManager + { + public const string FAILED_TEXT = "failed"; + + public Func CreateRequested; + + public event Action RoomsUpdated; + + public IBindableCollection Rooms { get; } = null; + + public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + { + if (CreateRequested == null) + return; + + if (!CreateRequested.Invoke(room)) + onError?.Invoke(FAILED_TEXT); + else + onSuccess?.Invoke(room); + } + + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => throw new NotImplementedException(); + + public void PartRoom() => throw new NotImplementedException(); + + public void Filter(FilterCriteria criteria) => throw new NotImplementedException(); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMultiHeader.cs b/osu.Game.Tests/Visual/TestCaseMultiHeader.cs index d27a447077..602108d078 100644 --- a/osu.Game.Tests/Visual/TestCaseMultiHeader.cs +++ b/osu.Game.Tests/Visual/TestCaseMultiHeader.cs @@ -3,8 +3,8 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Screens; using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Screens.Lounge; namespace osu.Game.Tests.Visual { @@ -13,15 +13,31 @@ namespace osu.Game.Tests.Visual { public TestCaseMultiHeader() { - Lounge lounge; + int index = 0; + + OsuScreen currentScreen = new TestMultiplayerSubScreen(index); + Children = new Drawable[] { - lounge = new Lounge - { - Padding = new MarginPadding { Top = Header.HEIGHT }, - }, - new Header(lounge), + currentScreen, + new Header(currentScreen) }; + + AddStep("push multi screen", () => currentScreen.Push(currentScreen = new TestMultiplayerSubScreen(++index))); + } + + private class TestMultiplayerSubScreen : OsuScreen, IMultiplayerSubScreen + { + private readonly int index; + + public string ShortTitle => $"Screen {index}"; + + public TestMultiplayerSubScreen(int index) + { + this.index = index; + } + + public override string ToString() => ShortTitle; } } } diff --git a/osu.Game.Tests/Visual/TestCaseMultiScreen.cs b/osu.Game.Tests/Visual/TestCaseMultiScreen.cs index 6c22fb020f..88265d146f 100644 --- a/osu.Game.Tests/Visual/TestCaseMultiScreen.cs +++ b/osu.Game.Tests/Visual/TestCaseMultiScreen.cs @@ -1,14 +1,25 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Game.Screens.Multi; +using osu.Game.Screens.Multi.Lounge; +using osu.Game.Screens.Multi.Lounge.Components; namespace osu.Game.Tests.Visual { [TestFixture] public class TestCaseMultiScreen : OsuTestCase { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(Multiplayer), + typeof(LoungeSubScreen), + typeof(FilterControl) + }; + public TestCaseMultiScreen() { Multiplayer multi = new Multiplayer(); diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index d87a8d0056..29060ceb12 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -87,10 +87,7 @@ namespace osu.Game.Tests.Visual usage.Migrate(); Dependencies.Cache(rulesets = new RulesetStore(factory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null) - { - DefaultBeatmap = defaultBeatmap = Beatmap.Default - }); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null, null, defaultBeatmap = Beatmap.Default)); Beatmap.SetDefault(); } diff --git a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs index de839a21af..6ec3bd108b 100644 --- a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs +++ b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual { Beatmap.Value = new DummyWorkingBeatmap(game); - AddStep("load dummy beatmap", () => Add(loader = new PlayerLoader(new Player + AddStep("load dummy beatmap", () => Add(loader = new PlayerLoader(() => new Player { AllowPause = false, AllowLeadIn = false, @@ -30,9 +30,9 @@ namespace osu.Game.Tests.Visual AddStep("load slow dummy beatmap", () => { - SlowLoadPlayer slow; + SlowLoadPlayer slow = null; - Add(loader = new PlayerLoader(slow = new SlowLoadPlayer + Add(loader = new PlayerLoader(() => slow = new SlowLoadPlayer { AllowPause = false, AllowLeadIn = false, diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs index e0ea613534..0fc4616f56 100644 --- a/osu.Game.Tests/Visual/TestCaseReplay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplay.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.Linq; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual @@ -23,7 +22,7 @@ namespace osu.Game.Tests.Visual // Reset the mods Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Where(m => !(m is ModAutoplay)); - return new ReplayPlayer(new Score { Replay = dummyRulesetContainer.Replay }); + return new ReplayPlayer(dummyRulesetContainer.ReplayScore); } } } diff --git a/osu.Game.Tests/Visual/TestCaseResults.cs b/osu.Game.Tests/Visual/TestCaseResults.cs index a954c6c57c..98aaa23beb 100644 --- a/osu.Game.Tests/Visual/TestCaseResults.cs +++ b/osu.Game.Tests/Visual/TestCaseResults.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual typeof(Results), typeof(ResultsPage), typeof(ScoreResultsPage), - typeof(RankingResultsPage) + typeof(LocalLeaderboardPage) }; [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs deleted file mode 100644 index 06b9c4a6f9..0000000000 --- a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Components; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual -{ - [TestFixture] - public class TestCaseRoomInspector : OsuTestCase - { - private RulesetStore rulesets; - - protected override void LoadComplete() - { - base.LoadComplete(); - - Room room = new Room - { - Name = { Value = @"My Awesome Room" }, - Host = { Value = new User { Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" } } }, - Status = { Value = new RoomStatusOpen() }, - Type = { Value = new GameTypeTeamVersus() }, - Beatmap = - { - Value = new BeatmapInfo - { - StarDifficulty = 3.7, - Ruleset = rulesets.GetRuleset(3), - Metadata = new BeatmapMetadata - { - Title = @"Platina", - Artist = @"Maaya Sakamoto", - AuthorString = @"uwutm8", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/560573/covers/cover.jpg?1492722343", - }, - }, - }, - } - }, - MaxParticipants = { Value = 200 }, - Participants = - { - Value = new[] - { - new User { Username = @"flyte", Id = 3103765, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 142 } } }, - new User { Username = @"Cookiezi", Id = 124493, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 546 } } }, - new User { Username = @"Angelsim", Id = 1777162, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 287 } } }, - new User { Username = @"Rafis", Id = 2558286, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 468 } } }, - new User { Username = @"hvick225", Id = 50265, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 325 } } }, - new User { Username = @"peppy", Id = 2, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 625 } } }, - } - } - }; - - RoomInspector inspector; - Add(inspector = new RoomInspector - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - }); - - AddStep(@"set room", () => inspector.Room = room); - AddStep(@"null room", () => inspector.Room = null); - AddStep(@"set room", () => inspector.Room = room); - AddStep(@"change title", () => room.Name.Value = @"A Better Room Than The Above"); - AddStep(@"change host", () => room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }); - AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying()); - AddStep(@"change type", () => room.Type.Value = new GameTypeTag()); - AddStep(@"change beatmap", () => room.Beatmap.Value = null); - AddStep(@"change max participants", () => room.MaxParticipants.Value = null); - AddStep(@"change participants", () => room.Participants.Value = new[] - { - new User { Username = @"filsdelama", Id = 2831793, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 854 } } }, - new User { Username = @"_index", Id = 652457, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 150 } } } - }); - - AddStep(@"change room", () => - { - Room newRoom = new Room - { - Name = { Value = @"My New, Better Than Ever Room" }, - Host = { Value = new User { Username = @"Angelsim", Id = 1777162, Country = new Country { FlagName = @"KR" } } }, - Status = { Value = new RoomStatusOpen() }, - Type = { Value = new GameTypeTagTeam() }, - Beatmap = - { - Value = new BeatmapInfo - { - StarDifficulty = 7.07, - Ruleset = rulesets.GetRuleset(0), - Metadata = new BeatmapMetadata - { - Title = @"FREEDOM DIVE", - Artist = @"xi", - AuthorString = @"Nakagawa-Kanon", - }, - BeatmapSet = new BeatmapSetInfo - { - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/39804/covers/cover.jpg?1456506845", - }, - }, - }, - }, - }, - MaxParticipants = { Value = 10 }, - Participants = - { - Value = new[] - { - new User { Username = @"Angelsim", Id = 1777162, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 4 } } }, - new User { Username = @"HappyStick", Id = 256802, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 752 } } }, - new User { Username = @"-Konpaku-", Id = 2258797, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 571 } } } - } - } - }; - - inspector.Room = newRoom; - }); - } - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - this.rulesets = rulesets; - } - } -} diff --git a/osu.Game.Tests/Visual/TestCaseRoomSettings.cs b/osu.Game.Tests/Visual/TestCaseRoomSettings.cs deleted file mode 100644 index 40742ce709..0000000000 --- a/osu.Game.Tests/Visual/TestCaseRoomSettings.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Testing.Input; -using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Screens.Match.Settings; -using osuTK.Input; - -namespace osu.Game.Tests.Visual -{ - [TestFixture] - public class TestCaseRoomSettings : ManualInputManagerTestCase - { - private readonly Room room; - private readonly TestRoomSettingsOverlay overlay; - - public TestCaseRoomSettings() - { - room = new Room - { - Name = { Value = "One Testing Room" }, - Availability = { Value = RoomAvailability.Public }, - Type = { Value = new GameTypeTeamVersus() }, - MaxParticipants = { Value = 10 }, - }; - - Add(overlay = new TestRoomSettingsOverlay(room) - { - RelativeSizeAxes = Axes.Both, - Height = 0.75f, - }); - - AddStep(@"show", overlay.Show); - assertAll(); - AddStep(@"set name", () => overlay.CurrentName = @"Two Testing Room"); - AddStep(@"set max", () => overlay.CurrentMaxParticipants = null); - AddStep(@"set availability", () => overlay.CurrentAvailability = RoomAvailability.InviteOnly); - AddStep(@"set type", () => overlay.CurrentType = new GameTypeTagTeam()); - apply(); - assertAll(); - AddStep(@"show", overlay.Show); - AddStep(@"set room name", () => room.Name.Value = @"Room Changed Name!"); - AddStep(@"set room availability", () => room.Availability.Value = RoomAvailability.Public); - AddStep(@"set room type", () => room.Type.Value = new GameTypeTag()); - AddStep(@"set room max", () => room.MaxParticipants.Value = 100); - assertAll(); - AddStep(@"set name", () => overlay.CurrentName = @"Unsaved Testing Room"); - AddStep(@"set max", () => overlay.CurrentMaxParticipants = 20); - AddStep(@"set availability", () => overlay.CurrentAvailability = RoomAvailability.FriendsOnly); - AddStep(@"set type", () => overlay.CurrentType = new GameTypeVersus()); - AddStep(@"hide", overlay.Hide); - AddWaitStep(5); - AddStep(@"show", overlay.Show); - assertAll(); - AddStep(@"hide", overlay.Hide); - } - - private void apply() - { - AddStep(@"apply", () => - { - overlay.ClickApplyButton(InputManager); - }); - } - - private void assertAll() - { - AddAssert(@"name == room name", () => overlay.CurrentName == room.Name.Value); - AddAssert(@"max == room max", () => overlay.CurrentMaxParticipants == room.MaxParticipants.Value); - AddAssert(@"availability == room availability", () => overlay.CurrentAvailability == room.Availability.Value); - AddAssert(@"type == room type", () => Equals(overlay.CurrentType, room.Type.Value)); - } - - private class TestRoomSettingsOverlay : RoomSettingsOverlay - { - public string CurrentName - { - get => NameField.Text; - set => NameField.Text = value; - } - - public int? CurrentMaxParticipants - { - get - { - if (int.TryParse(MaxParticipantsField.Text, out int max)) - return max; - - return null; - } - set => MaxParticipantsField.Text = value?.ToString(); - } - - public RoomAvailability CurrentAvailability - { - get => AvailabilityPicker.Current.Value; - set => AvailabilityPicker.Current.Value = value; - } - - public GameType CurrentType - { - get => TypePicker.Current.Value; - set => TypePicker.Current.Value = value; - } - - public TestRoomSettingsOverlay(Room room) : base(room) - { - } - - public void ClickApplyButton(ManualInputManager inputManager) - { - inputManager.MoveMouseTo(ApplyButton); - inputManager.Click(MouseButton.Left); - } - } - } -} diff --git a/osu.Game.Tests/Visual/TestCaseRoomStatus.cs b/osu.Game.Tests/Visual/TestCaseRoomStatus.cs new file mode 100644 index 0000000000..d44b399a95 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseRoomStatus.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseRoomStatus : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(RoomStatusEnded), + typeof(RoomStatusOpen), + typeof(RoomStatusPlaying) + }; + + public TestCaseRoomStatus() + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Children = new Drawable[] + { + new DrawableRoom(new Room + { + Name = { Value = "Room 1" }, + Status = { Value = new RoomStatusOpen() } + }), + new DrawableRoom(new Room + { + Name = { Value = "Room 2" }, + Status = { Value = new RoomStatusPlaying() } + }), + new DrawableRoom(new Room + { + Name = { Value = "Room 3" }, + Status = { Value = new RoomStatusEnded() } + }), + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs new file mode 100644 index 0000000000..bc449f645b --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Tests.Beatmaps.IO; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseUpdateableBeatmapBackgroundSprite : OsuTestCase + { + private UpdateableBeatmapBackgroundSprite backgroundSprite; + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osu, APIAccess api, RulesetStore rulesets) + { + Bindable beatmapBindable = new Bindable(); + + var imported = ImportBeatmapTest.LoadOszIntoOsu(osu); + + Child = backgroundSprite = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }; + + backgroundSprite.Beatmap.BindTo(beatmapBindable); + + var req = new GetBeatmapSetRequest(1); + api.Queue(req); + + AddStep("null", () => beatmapBindable.Value = null); + + AddStep("imported", () => beatmapBindable.Value = imported.Beatmaps.First()); + + if (api.IsLoggedIn) + { + AddUntilStep(() => req.Result != null, "wait for api response"); + + AddStep("online", () => beatmapBindable.Value = new BeatmapInfo + { + BeatmapSet = req.Result?.ToBeatmapSet(rulesets) + }); + } + else + { + AddStep("online (login first)", () => { }); + } + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c179821a7c..73fd5da22c 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. /// - public WorkingBeatmap DefaultBeatmap { private get; set; } + public readonly WorkingBeatmap DefaultBeatmap; public override string[] HandledExtensions => new[] { ".osz" }; @@ -77,16 +77,19 @@ namespace osu.Game.Beatmaps private readonly List currentDownloads = new List(); - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null) + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null, + WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, new BeatmapStore(contextFactory), importHost) { - beatmaps = (BeatmapStore)ModelStore; - beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - this.rulesets = rulesets; this.api = api; this.audioManager = audioManager; + + DefaultBeatmap = defaultBeatmap; + + beatmaps = (BeatmapStore)ModelStore; + beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); + beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); } protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs index 883c05f1e4..e09da2fb72 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs @@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps.Drawables } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { string resource = null; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 14162c35c0..fe6200472f 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -16,15 +17,16 @@ namespace osu.Game.Beatmaps.Drawables { public class DifficultyIcon : DifficultyColouredContainer { - private readonly BeatmapInfo beatmap; + private readonly RulesetInfo ruleset; - public DifficultyIcon(BeatmapInfo beatmap) + public DifficultyIcon(BeatmapInfo beatmap, RulesetInfo ruleset = null) : base(beatmap) { if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); - this.beatmap = beatmap; + this.ruleset = ruleset ?? beatmap.Ruleset; + Size = new Vector2(20); } @@ -58,7 +60,7 @@ namespace osu.Game.Beatmaps.Drawables Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) - Icon = beatmap.Ruleset?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.fa_question_circle_o } + Icon = ruleset?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.fa_question_circle_o } } }; } diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs new file mode 100644 index 0000000000..724c6d656a --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Beatmaps.Drawables +{ + /// + /// Display a baetmap background from a local source, but fallback to online source if not available. + /// + public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable + { + public readonly IBindable Beatmap = new Bindable(); + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + public UpdateableBeatmapBackgroundSprite() + { + Beatmap.BindValueChanged(b => Model = b); + } + + protected override Drawable CreateDrawable(BeatmapInfo model) + { + return new DelayedLoadUnloadWrapper(() => { + Drawable drawable; + + var localBeatmap = beatmaps.GetWorkingBeatmap(model); + + if (localBeatmap.BeatmapInfo.ID == 0 && model?.BeatmapSet?.OnlineInfo != null) + drawable = new BeatmapSetCover(model.BeatmapSet); + else + drawable = new BeatmapBackgroundSprite(localBeatmap); + + drawable.RelativeSizeAxes = Axes.Both; + drawable.Anchor = Anchor.Centre; + drawable.Origin = Anchor.Centre; + drawable.FillMode = FillMode.Fill; + drawable.OnLoadComplete = d => d.FadeInFromZero(400); + + return drawable; + }, 500, 10000); + } + + protected override double FadeDuration => 0; + } +} diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 54a2ea47f9..74315d2522 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -61,11 +62,25 @@ namespace osu.Game.Graphics.Containers } public void AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action creationParameters = null) + => createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText); + + public void AddLink(string text, Action action, string tooltipText = null, Action creationParameters = null) + => createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action); + + public void AddLink(IEnumerable text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null) { - AddInternal(new DrawableLinkCompiler(AddText(text, creationParameters).ToList()) + foreach (var t in text) + AddArbitraryDrawable(t); + + createLink(text, null, url, linkType, linkArgument, tooltipText); + } + + private void createLink(IEnumerable drawables, string text, string url = null, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action action = null) + { + AddInternal(new DrawableLinkCompiler(drawables.OfType().ToList()) { TooltipText = tooltipText ?? (url != text ? url : string.Empty), - Action = () => + Action = action ?? (() => { switch (linkType) { @@ -104,7 +119,7 @@ namespace osu.Game.Graphics.Containers default: throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action."); } - }, + }), }); } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8038c1fc1f..10b4e73419 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -145,7 +145,8 @@ namespace osu.Game.Online.API if (!handleRequest(userReq)) { - Thread.Sleep(500); + if (State == APIState.Connecting) + State = APIState.Failing; continue; } diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 41f774e83c..9a1238b019 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -36,7 +36,7 @@ namespace osu.Game.Online.API /// public abstract class APIRequest { - protected virtual string Target => string.Empty; + protected abstract string Target { get; } protected virtual WebRequest CreateWebRequest() => new WebRequest(Uri); @@ -92,7 +92,11 @@ namespace osu.Game.Online.API public void Fail(Exception e) { - if (cancelled) return; + if (WebRequest?.Completed == true) + return; + + if (cancelled) + return; cancelled = true; WebRequest?.Abort(); diff --git a/osu.Game/Online/API/Requests/CreateRoomRequest.cs b/osu.Game/Online/API/Requests/CreateRoomRequest.cs new file mode 100644 index 0000000000..fc401be5f1 --- /dev/null +++ b/osu.Game/Online/API/Requests/CreateRoomRequest.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Net.Http; +using Newtonsoft.Json; +using osu.Framework.IO.Network; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Online.API.Requests +{ + public class CreateRoomRequest : APIRequest + { + private readonly Room room; + + public CreateRoomRequest(Room room) + { + this.room = room; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.ContentType = "application/json"; + req.Method = HttpMethod.Post; + + req.AddRaw(JsonConvert.SerializeObject(room)); + + return req; + } + + protected override string Target => "rooms"; + } +} diff --git a/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs b/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs new file mode 100644 index 0000000000..0e99b53ec0 --- /dev/null +++ b/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class CreateRoomScoreRequest : APIRequest + { + private readonly int roomId; + private readonly int playlistItemId; + + public CreateRoomScoreRequest(int roomId, int playlistItemId) + { + this.roomId = roomId; + this.playlistItemId = playlistItemId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + return req; + } + + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores"; + } +} diff --git a/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs b/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs new file mode 100644 index 0000000000..1699694878 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetRoomScoresRequest : APIRequest> + { + private readonly int roomId; + + public GetRoomScoresRequest(int roomId) + { + this.roomId = roomId; + } + + protected override string Target => $@"rooms/{roomId}/leaderboard"; + } +} diff --git a/osu.Game/Online/API/Requests/GetRoomsRequest.cs b/osu.Game/Online/API/Requests/GetRoomsRequest.cs new file mode 100644 index 0000000000..bc835b1ab4 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetRoomsRequest.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Online.API.Requests +{ + public class GetRoomsRequest : APIRequest> + { + private readonly PrimaryFilter primaryFilter; + + public GetRoomsRequest(PrimaryFilter primaryFilter) + { + this.primaryFilter = primaryFilter; + } + + protected override string Target + { + get + { + string target = "rooms"; + + switch (primaryFilter) + { + case PrimaryFilter.Open: + break; + case PrimaryFilter.Owned: + target += "/owned"; + break; + case PrimaryFilter.Participated: + target += "/participated"; + break; + case PrimaryFilter.RecentlyEnded: + target += "/ended"; + break; + } + + return target; + } + } + } +} diff --git a/osu.Game/Online/API/Requests/JoinRoomRequest.cs b/osu.Game/Online/API/Requests/JoinRoomRequest.cs new file mode 100644 index 0000000000..b5e162da08 --- /dev/null +++ b/osu.Game/Online/API/Requests/JoinRoomRequest.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Online.API.Requests +{ + public class JoinRoomRequest : APIRequest + { + private readonly Room room; + private readonly User user; + + public JoinRoomRequest(Room room, User user) + { + this.room = room; + this.user = user; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Put; + return req; + } + + protected override string Target => $"rooms/{room.RoomID.Value}/users/{user.Id}"; + } +} diff --git a/osu.Game/Online/API/Requests/PartRoomRequest.cs b/osu.Game/Online/API/Requests/PartRoomRequest.cs new file mode 100644 index 0000000000..5e4457f69f --- /dev/null +++ b/osu.Game/Online/API/Requests/PartRoomRequest.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Online.API.Requests +{ + public class PartRoomRequest : APIRequest + { + private readonly Room room; + private readonly User user; + + public PartRoomRequest(Room room, User user) + { + this.room = room; + this.user = user; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Delete; + return req; + } + + protected override string Target => $"rooms/{room.RoomID.Value}/users/{user.Id}"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 193ccf1f6b..7b42672c2e 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -59,19 +59,17 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapInfo ToBeatmap(RulesetStore rulesets) { + var set = BeatmapSet?.ToBeatmapSet(rulesets); + return new BeatmapInfo { - Metadata = this, + Metadata = set?.Metadata ?? this, Ruleset = rulesets.GetRuleset(ruleset), StarDifficulty = starDifficulty, OnlineBeatmapID = OnlineBeatmapID, Version = version, Status = Status, - BeatmapSet = new BeatmapSetInfo - { - OnlineBeatmapSetID = OnlineBeatmapSetID, - Status = BeatmapSet?.Status ?? BeatmapSetOnlineStatus.None - }, + BeatmapSet = set, BaseDifficulty = new BeatmapDifficulty { DrainRate = drainRate, diff --git a/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs b/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs new file mode 100644 index 0000000000..ca0daa04b6 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APICreatedRoom : Room + { + [JsonProperty("error")] + public string Error { get; set; } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIMod.cs b/osu.Game/Online/API/Requests/Responses/APIMod.cs new file mode 100644 index 0000000000..af0d0c643f --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIMod.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIMod : IMod + { + public string Acronym { get; set; } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIRoomScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APIRoomScoreInfo.cs new file mode 100644 index 0000000000..c6ef919e54 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIRoomScoreInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Scoring; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIRoomScoreInfo : ScoreInfo + { + [JsonProperty("attempts")] + public int TotalAttempts { get; set; } + + [JsonProperty("completed")] + public int CompletedBeatmaps { get; set; } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs b/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs new file mode 100644 index 0000000000..6a0ecd37c7 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIScoreToken + { + [JsonProperty("id")] + public int ID { get; set; } + } +} diff --git a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs b/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs new file mode 100644 index 0000000000..d25e35db58 --- /dev/null +++ b/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Net.Http; +using Newtonsoft.Json; +using osu.Framework.IO.Network; +using osu.Game.Scoring; + +namespace osu.Game.Online.API.Requests +{ + public class SubmitRoomScoreRequest : APIRequest + { + private readonly int scoreId; + private readonly int roomId; + private readonly int playlistItemId; + private readonly ScoreInfo scoreInfo; + + public SubmitRoomScoreRequest(int scoreId, int roomId, int playlistItemId, ScoreInfo scoreInfo) + { + this.scoreId = scoreId; + this.roomId = roomId; + this.playlistItemId = playlistItemId; + this.scoreInfo = scoreInfo; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.ContentType = "application/json"; + req.Method = HttpMethod.Put; + + req.AddRaw(JsonConvert.SerializeObject(scoreInfo)); + + return req; + } + + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}"; + } +} diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 1ec138ab57..576a1d619a 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -19,10 +19,10 @@ namespace osu.Game.Online.Chat /// public class StandAloneChatDisplay : CompositeDrawable { - private readonly bool postingTextbox; - public readonly Bindable Channel = new Bindable(); + public Action Exit; + private readonly FocusedTextBox textbox; protected ChannelManager ChannelManager; @@ -31,6 +31,8 @@ namespace osu.Game.Online.Chat private DrawableChannel drawableChannel; + private readonly bool postingTextbox; + private const float textbox_height = 30; /// @@ -66,6 +68,8 @@ namespace osu.Game.Online.Chat Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, }); + + textbox.Exit += () => Exit?.Invoke(); } Channel.BindValueChanged(channelChanged); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index d9d78245bb..f3bf16a05f 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Online.Leaderboards { - public abstract class Leaderboard : Container + public abstract class Leaderboard : Container, IOnlineComponent { private const double fade_duration = 300; @@ -55,14 +55,8 @@ namespace osu.Game.Online.Leaderboards // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; - var flow = scrollFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 5f), - Padding = new MarginPadding { Top = 10, Bottom = 5 }, - ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)) - }; + scrollFlow = CreateScoreFlow(); + scrollFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); // schedule because we may not be loaded yet (LoadComponentAsync complains). showScoresDelegate?.Cancel(); @@ -71,12 +65,12 @@ namespace osu.Game.Online.Leaderboards else showScores(); - void showScores() => LoadComponentAsync(flow, _ => + void showScores() => LoadComponentAsync(scrollFlow, _ => { - scrollContainer.Add(flow); + scrollContainer.Add(scrollFlow); int i = 0; - foreach (var s in flow.Children) + foreach (var s in scrollFlow.Children) { using (s.BeginDelayedSequence(i++ * 50, true)) s.Show(); @@ -87,6 +81,15 @@ namespace osu.Game.Online.Leaderboards } } + protected virtual FillFlowContainer CreateScoreFlow() + => new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 5f), + Padding = new MarginPadding { Top = 10, Bottom = 5 }, + }; + private TScope scope; public TScope Scope @@ -175,26 +178,22 @@ namespace osu.Game.Online.Leaderboards private void load(APIAccess api) { this.api = api; - - if (api != null) - api.OnStateChange += handleApiStateChange; + api?.Register(this); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - - if (api != null) - api.OnStateChange -= handleApiStateChange; + api?.Unregister(this); } public void RefreshScores() => UpdateScores(); private APIRequest getScoresRequest; - private void handleApiStateChange(APIState oldState, APIState newState) + public void APIStateChanged(APIAccess api, APIState state) { - if (newState == APIState.Online) + if (state == APIState.Online) UpdateScores(); } @@ -265,14 +264,18 @@ namespace osu.Game.Online.Leaderboards currentPlaceholder = placeholder; } + protected virtual bool FadeBottom => true; + protected virtual bool FadeTop => false; + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - var fadeStart = scrollContainer.Current + scrollContainer.DrawHeight; + var fadeBottom = scrollContainer.Current + scrollContainer.DrawHeight; + var fadeTop = scrollContainer.Current + LeaderboardScore.HEIGHT; if (!scrollContainer.IsScrolledToEnd()) - fadeStart -= LeaderboardScore.HEIGHT; + fadeBottom -= LeaderboardScore.HEIGHT; if (scrollFlow == null) return; @@ -282,15 +285,23 @@ namespace osu.Game.Online.Leaderboards var topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scrollFlow).Y; var bottomY = topY + LeaderboardScore.HEIGHT; - if (bottomY < fadeStart) + bool requireTopFade = FadeTop && topY <= fadeTop; + bool requireBottomFade = FadeBottom && bottomY >= fadeBottom; + + if (!requireTopFade && !requireBottomFade) c.Colour = Color4.White; - else if (topY > fadeStart + LeaderboardScore.HEIGHT) + else if (topY > fadeBottom + LeaderboardScore.HEIGHT || bottomY < fadeTop - LeaderboardScore.HEIGHT) c.Colour = Color4.Transparent; else { - c.Colour = ColourInfo.GradientVertical( - Color4.White.Opacity(Math.Min(1 - (topY - fadeStart) / LeaderboardScore.HEIGHT, 1)), - Color4.White.Opacity(Math.Min(1 - (bottomY - fadeStart) / LeaderboardScore.HEIGHT, 1))); + if (bottomY - fadeBottom > 0 && FadeBottom) + c.Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / LeaderboardScore.HEIGHT, 1)), + Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / LeaderboardScore.HEIGHT, 1))); + else if (FadeTop) + c.Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / LeaderboardScore.HEIGHT, 1)), + Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / LeaderboardScore.HEIGHT, 1))); } } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0c64105d5c..e60a73467b 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -73,8 +73,8 @@ namespace osu.Game.Online.Leaderboards { new OsuSpriteText { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Font = @"Exo2.0-MediumItalic", TextSize = 22, // ReSharper disable once ImpureMethodCallOnReadonlyValueField diff --git a/osu.Game/Online/Multiplayer/GameType.cs b/osu.Game/Online/Multiplayer/GameType.cs index 8d39e8f59d..e800142f9b 100644 --- a/osu.Game/Online/Multiplayer/GameType.cs +++ b/osu.Game/Online/Multiplayer/GameType.cs @@ -1,11 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osuTK; -using osuTK.Graphics; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; namespace osu.Game.Online.Multiplayer @@ -13,137 +9,10 @@ namespace osu.Game.Online.Multiplayer public abstract class GameType { public abstract string Name { get; } + public abstract Drawable GetIcon(OsuColour colours, float size); public override int GetHashCode() => GetType().GetHashCode(); public override bool Equals(object obj) => GetType() == obj?.GetType(); } - - public class GameTypeTag : GameType - { - public override string Name => "Tag"; - public override Drawable GetIcon(OsuColour colours, float size) - { - return new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = FontAwesome.fa_refresh, - Size = new Vector2(size), - Colour = colours.Blue, - Shadow = false, - }; - } - } - - public class GameTypeVersus : GameType - { - public override string Name => "Versus"; - public override Drawable GetIcon(OsuColour colours, float size) - { - return new VersusRow(colours.Blue, colours.Blue, size * 0.6f) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - } - } - - public class GameTypeTagTeam : GameType - { - public override string Name => "Tag Team"; - public override Drawable GetIcon(OsuColour colours, float size) - { - return new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(2f), - Children = new[] - { - new SpriteIcon - { - Icon = FontAwesome.fa_refresh, - Size = new Vector2(size * 0.75f), - Colour = colours.Blue, - Shadow = false, - }, - new SpriteIcon - { - Icon = FontAwesome.fa_refresh, - Size = new Vector2(size * 0.75f), - Colour = colours.Pink, - Shadow = false, - }, - }, - }; - } - } - - public class GameTypeTeamVersus : GameType - { - public override string Name => "Team Versus"; - public override Drawable GetIcon(OsuColour colours, float size) - { - return new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2f), - Children = new[] - { - new VersusRow(colours.Blue, colours.Pink, size * 0.5f), - new VersusRow(colours.Blue, colours.Pink, size * 0.5f), - }, - }; - } - } - - public class VersusRow : FillFlowContainer - { - public VersusRow(Color4 first, Color4 second, float size) - { - var triangleSize = new Vector2(size); - AutoSizeAxes = Axes.Both; - Spacing = new Vector2(2f, 0f); - - Children = new[] - { - new Container - { - Size = triangleSize, - Colour = first, - Children = new[] - { - new EquilateralTriangle - { - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Rotation = 90, - EdgeSmoothness = new Vector2(1f), - }, - }, - }, - new Container - { - Size = triangleSize, - Colour = second, - Children = new[] - { - new EquilateralTriangle - { - Anchor = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Rotation = -90, - EdgeSmoothness = new Vector2(1f), - }, - }, - }, - }; - } - } } diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs new file mode 100644 index 0000000000..c9d16662ba --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Online.Multiplayer.GameTypes +{ + public class GameTypeTag : GameType + { + public override string Name => "Tag"; + + public override Drawable GetIcon(OsuColour colours, float size) + { + return new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.fa_refresh, + Size = new Vector2(size), + Colour = colours.Blue, + Shadow = false, + }; + } + } +} diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs new file mode 100644 index 0000000000..952d1a39ac --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Online.Multiplayer.GameTypes +{ + public class GameTypeTagTeam : GameType + { + public override string Name => "Tag Team"; + + public override Drawable GetIcon(OsuColour colours, float size) + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f), + Children = new[] + { + new SpriteIcon + { + Icon = FontAwesome.fa_refresh, + Size = new Vector2(size * 0.75f), + Colour = colours.Blue, + Shadow = false, + }, + new SpriteIcon + { + Icon = FontAwesome.fa_refresh, + Size = new Vector2(size * 0.75f), + Colour = colours.Pink, + Shadow = false, + }, + }, + }; + } + } +} diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs new file mode 100644 index 0000000000..49e7cef88d --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Online.Multiplayer.GameTypes +{ + public class GameTypeTeamVersus : GameType + { + public override string Name => "Team Versus"; + + public override Drawable GetIcon(OsuColour colours, float size) + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2f), + Children = new[] + { + new VersusRow(colours.Blue, colours.Pink, size * 0.5f), + new VersusRow(colours.Blue, colours.Pink, size * 0.5f), + }, + }; + } + } +} diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs new file mode 100644 index 0000000000..ab8658dfb2 --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Online.Multiplayer.GameTypes +{ + public class GameTypeTimeshift : GameType + { + public override string Name => "Timeshift"; + + public override Drawable GetIcon(OsuColour colours, float size) => new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.fa_clock_o, + Size = new Vector2(size), + Colour = colours.Blue, + Shadow = false + }; + } +} diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs b/osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs new file mode 100644 index 0000000000..c74ce63470 --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Game.Graphics; + +namespace osu.Game.Online.Multiplayer.GameTypes +{ + public class GameTypeVersus : GameType + { + public override string Name => "Versus"; + + public override Drawable GetIcon(OsuColour colours, float size) + { + return new VersusRow(colours.Blue, colours.Blue, size * 0.6f) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs b/osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs new file mode 100644 index 0000000000..b4d8279d02 --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Online.Multiplayer.GameTypes +{ + public class VersusRow : FillFlowContainer + { + public VersusRow(Color4 first, Color4 second, float size) + { + var triangleSize = new Vector2(size); + AutoSizeAxes = Axes.Both; + Spacing = new Vector2(2f, 0f); + + Children = new[] + { + new Container + { + Size = triangleSize, + Colour = first, + Children = new[] + { + new EquilateralTriangle + { + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Rotation = 90, + EdgeSmoothness = new Vector2(1f), + }, + }, + }, + new Container + { + Size = triangleSize, + Colour = second, + Children = new[] + { + new EquilateralTriangle + { + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Rotation = -90, + EdgeSmoothness = new Vector2(1f), + }, + }, + }, + }; + } + } +} diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Multiplayer/PlaylistItem.cs new file mode 100644 index 0000000000..4155121bdf --- /dev/null +++ b/osu.Game/Online/Multiplayer/PlaylistItem.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using Newtonsoft.Json; +using osu.Framework.Configuration; +using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Online.Multiplayer +{ + public class PlaylistItem + { + [JsonProperty("id")] + public int ID { get; set; } + + [JsonProperty("beatmap_id")] + public int BeatmapID { get; set; } + + [JsonProperty("ruleset_id")] + public int RulesetID { get; set; } + + [JsonIgnore] + public BeatmapInfo Beatmap + { + get => beatmap; + set + { + beatmap = value; + BeatmapID = value?.OnlineBeatmapID ?? 0; + } + } + + [JsonIgnore] + public RulesetInfo Ruleset { get; set; } + + [JsonIgnore] + public readonly BindableCollection AllowedMods = new BindableCollection(); + + [JsonIgnore] + public readonly BindableCollection RequiredMods = new BindableCollection(); + + [JsonProperty("beatmap")] + private APIBeatmap apiBeatmap { get; set; } + + [JsonProperty("allowed_mods")] + private APIMod[] allowedMods + { + get => AllowedMods.Select(m => new APIMod { Acronym = m.Acronym }).ToArray(); + set => _allowedMods = value; + } + + [JsonProperty("required_mods")] + private APIMod[] requiredMods + { + get => RequiredMods.Select(m => new APIMod { Acronym = m.Acronym }).ToArray(); + set => _requiredMods = value; + } + + private BeatmapInfo beatmap; + private APIMod[] _allowedMods; + private APIMod[] _requiredMods; + + public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) + { + // If we don't have an api beatmap, the request occurred as a result of room creation, so we can query the local beatmap instead + // Todo: Is this a bug? Room creation only returns the beatmap ID + Beatmap = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets); + Ruleset = rulesets.GetRuleset(RulesetID); + + if (_allowedMods != null) + { + AllowedMods.Clear(); + AllowedMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => _allowedMods.Any(m => m.Acronym == mod.Acronym))); + + _allowedMods = null; + } + + if (_requiredMods != null) + { + RequiredMods.Clear(); + RequiredMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => _requiredMods.Any(m => m.Acronym == mod.Acronym))); + + _requiredMods = null; + } + } + + public bool ShouldSerializeID() => false; + public bool ShouldSerializeapiBeatmap() => false; + } +} diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index b076afbcdb..448f5ced91 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -1,22 +1,120 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; using osu.Framework.Configuration; -using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer.GameTypes; +using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Users; namespace osu.Game.Online.Multiplayer { public class Room { - public Bindable Name = new Bindable(); - public Bindable Host = new Bindable(); - public Bindable Status = new Bindable(); - public Bindable Availability = new Bindable(); - public Bindable Type = new Bindable(); - public Bindable Beatmap = new Bindable(); - public Bindable MaxParticipants = new Bindable(); - public Bindable> Participants = new Bindable>(); + [JsonProperty("id")] + public Bindable RoomID { get; private set; } = new Bindable(); + + [JsonProperty("name")] + public Bindable Name { get; private set; } = new Bindable(); + + [JsonProperty("host")] + public Bindable Host { get; private set; } = new Bindable(); + + [JsonProperty("playlist")] + public BindableCollection Playlist { get; set; } = new BindableCollection(); + + [JsonProperty("channel_id")] + public Bindable ChannelId { get; private set; } = new Bindable(); + + [JsonIgnore] + public Bindable Duration { get; private set; } = new Bindable(TimeSpan.FromMinutes(30)); + + [JsonIgnore] + public Bindable MaxAttempts { get; private set; } = new Bindable(); + + [JsonIgnore] + public Bindable Status { get; private set; } = new Bindable(new RoomStatusOpen()); + + [JsonIgnore] + public Bindable Availability { get; private set; } = new Bindable(); + + [JsonIgnore] + public Bindable Type { get; private set; } = new Bindable(new GameTypeTimeshift()); + + [JsonIgnore] + public Bindable MaxParticipants { get; private set; } = new Bindable(); + + [JsonIgnore] + public Bindable> Participants { get; private set; } = new Bindable>(Enumerable.Empty()); + + public Bindable ParticipantCount { get; private set; } = new Bindable(); + + // todo: TEMPORARY + [JsonProperty("participant_count")] + private int? participantCount + { + get => ParticipantCount; + set => ParticipantCount.Value = value ?? 0; + } + + [JsonProperty("duration")] + private int duration + { + get => (int)Duration.Value.TotalMinutes; + set => Duration.Value = TimeSpan.FromMinutes(value); + } + + // Only supports retrieval for now + [JsonProperty("ends_at")] + public Bindable EndDate { get; private set; } = new Bindable(); + + // Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930) + [JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)] + private int? maxAttempts + { + get => MaxAttempts; + set => MaxAttempts.Value = value; + } + + /// + /// The position of this in the list. This is not read from or written to the API. + /// + [JsonIgnore] + public int Position = -1; + + public void CopyFrom(Room other) + { + RoomID.Value = other.RoomID; + Name.Value = other.Name; + + if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) + Host.Value = other.Host; + + Status.Value = other.Status; + Availability.Value = other.Availability; + Type.Value = other.Type; + MaxParticipants.Value = other.MaxParticipants; + ParticipantCount.Value = other.ParticipantCount.Value; + Participants.Value = other.Participants.Value.ToArray(); + EndDate.Value = other.EndDate; + + if (DateTimeOffset.Now >= EndDate.Value) + Status.Value = new RoomStatusEnded(); + + // Todo: Temporary, should only remove/add new items (requires framework changes) + if (Playlist.Count == 0) + Playlist.AddRange(other.Playlist); + else if (other.Playlist.Count > 0) + Playlist.First().ID = other.Playlist.First().ID; + + Position = other.Position; + } + + public bool ShouldSerializeRoomID() => false; + public bool ShouldSerializeHost() => false; + public bool ShouldSerializeEndDate() => false; } } diff --git a/osu.Game/Online/Multiplayer/RoomStatus.cs b/osu.Game/Online/Multiplayer/RoomStatus.cs index 7c6e71a360..f5c339c71f 100644 --- a/osu.Game/Online/Multiplayer/RoomStatus.cs +++ b/osu.Game/Online/Multiplayer/RoomStatus.cs @@ -10,17 +10,8 @@ namespace osu.Game.Online.Multiplayer { public abstract string Message { get; } public abstract Color4 GetAppropriateColour(OsuColour colours); - } - public class RoomStatusOpen : RoomStatus - { - public override string Message => @"Welcoming Players"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; - } - - public class RoomStatusPlaying : RoomStatus - { - public override string Message => @"Now Playing"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple; + public override int GetHashCode() => GetType().GetHashCode(); + public override bool Equals(object obj) => GetType() == obj?.GetType(); } } diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs b/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs new file mode 100644 index 0000000000..e6ab3ab495 --- /dev/null +++ b/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Online.Multiplayer.RoomStatuses +{ + public class RoomStatusEnded : RoomStatus + { + public override string Message => @"Ended"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker; + } +} diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs b/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs new file mode 100644 index 0000000000..6bc701feda --- /dev/null +++ b/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Online.Multiplayer.RoomStatuses +{ + public class RoomStatusOpen : RoomStatus + { + public override string Message => @"Welcoming Players"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; + } +} diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs b/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs new file mode 100644 index 0000000000..a725aca03a --- /dev/null +++ b/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Online.Multiplayer.RoomStatuses +{ + public class RoomStatusPlaying : RoomStatus + { + public override string Message => @"Now Playing"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple; + } +} diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 9d0bed7595..6296608e5d 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -39,7 +39,7 @@ namespace osu.Game.Online /// /// /// - /// The initial time in milliseconds to wait between polls. Setting to zero stops al polling. + /// The initial time in milliseconds to wait between polls. Setting to zero stops all polling. protected PollingComponent(double timeBetweenPolls = 0) { TimeBetweenPolls = timeBetweenPolls; @@ -87,13 +87,22 @@ namespace osu.Game.Online } /// - /// Perform the polling in this method. Call when done. + /// Performs a poll. Implement but do not call this. /// protected virtual Task Poll() { return Task.CompletedTask; } + /// + /// Immediately performs a . + /// + public void PollImmediately() + { + lastTimePolled = Time.Current - timeBetweenPolls; + scheduleNextPoll(); + } + /// /// Call when a poll operation has completed. /// @@ -103,7 +112,7 @@ namespace osu.Game.Online pollingActive = false; if (scheduledPoll == null) - scheduleNextPoll(); + pollIfNecessary(); } private void scheduleNextPoll() diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4930f75620..2a4c812401 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -317,7 +317,7 @@ namespace osu.Game Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); Beatmap.Value.Mods.Value = databasedScoreInfo.Mods; - currentScreen.Push(new PlayerLoader(new ReplayPlayer(databasedScore))); + currentScreen.Push(new PlayerLoader(() => new ReplayPlayer(databasedScore))); } } @@ -690,10 +690,41 @@ namespace osu.Game MenuCursorContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; } - private void screenAdded(Screen newScreen) + /// + /// Sets while ignoring any beatmap. + /// + /// The beatmap to set. + public void ForcefullySetBeatmap(WorkingBeatmap beatmap) + { + var beatmapDisabled = Beatmap.Disabled; + + Beatmap.Disabled = false; + Beatmap.Value = beatmap; + Beatmap.Disabled = beatmapDisabled; + } + + /// + /// Sets while ignoring any ruleset restrictions. + /// + /// The beatmap to set. + public void ForcefullySetRuleset(RulesetInfo ruleset) + { + var rulesetDisabled = this.ruleset.Disabled; + + this.ruleset.Disabled = false; + this.ruleset.Value = ruleset; + this.ruleset.Disabled = rulesetDisabled; + } + + protected virtual void ScreenChanged(OsuScreen current, Screen newScreen) { currentScreen = (OsuScreen)newScreen; - Logger.Log($"Screen changed → {currentScreen}"); + } + + private void screenAdded(Screen newScreen) + { + ScreenChanged(currentScreen, newScreen); + Logger.Log($"Screen changed → {newScreen}"); newScreen.ModePushed += screenAdded; newScreen.Exited += screenRemoved; @@ -701,7 +732,7 @@ namespace osu.Game private void screenRemoved(Screen newScreen) { - currentScreen = (OsuScreen)newScreen; + ScreenChanged(currentScreen, newScreen); Logger.Log($"Screen changed ← {currentScreen}"); if (newScreen == null) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0f1819d55b..683fa30818 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -153,9 +153,12 @@ namespace osu.Game dependencies.Cache(API); dependencies.CacheAs(API); + var defaultBeatmap = new DummyWorkingBeatmap(this); + beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap)); dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, BeatmapManager, Host.Storage, contextFactory, Host)); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); @@ -166,10 +169,6 @@ namespace osu.Game fileImporters.Add(ScoreManager); fileImporters.Add(SkinManager); - var defaultBeatmap = new DummyWorkingBeatmap(this); - beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio); - BeatmapManager.DefaultBeatmap = defaultBeatmap; - // tracks play so loud our samples can't keep up. // this adds a global reduction of track volume for the time being. Audio.Track.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); @@ -261,7 +260,7 @@ namespace osu.Game RegisterAudioManager(audioManager); } - private OsuBindableBeatmap(WorkingBeatmap defaultValue) + public OsuBindableBeatmap(WorkingBeatmap defaultValue) : base(defaultValue) { } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 40a395535d..9368bee81c 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -110,7 +112,7 @@ namespace osu.Game.Overlays.Music { sprite.TextSize = 16; sprite.Font = @"Exo2.0-Regular"; - }); + }).OfType(); text.AddText(artistBind.Value, sprite => { diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 0cb6d2d1aa..163eced103 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; using Container = osu.Framework.Graphics.Containers.Container; @@ -86,25 +87,29 @@ namespace osu.Game.Overlays.Settings.Sections.General }; break; case APIState.Failing: - Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Connection failing :(", - }, - }; - break; case APIState.Connecting: + LinkFlowContainer linkFlow; + Children = new Drawable[] { - new OsuSpriteText + new LoadingAnimation { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Connecting...", + State = Visibility.Visible, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + linkFlow = new LinkFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextAnchor = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Text = state == APIState.Failing ? "Connection is failing, will attempt to reconnect... " : "Attempting to connect... ", Margin = new MarginPadding { Top = 10, Bottom = 10 }, }, }; + + linkFlow.AddLink("cancel", api.Logout, string.Empty); break; case APIState.Online: Children = new Drawable[] diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index d0c4ce2f4c..49a3963496 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using Newtonsoft.Json; + namespace osu.Game.Rulesets.Mods { public interface IMod @@ -8,6 +10,7 @@ namespace osu.Game.Rulesets.Mods /// /// The shortened name of this mod. /// + [JsonProperty("acronym")] string Acronym { get; } } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 932439618d..72ae88d67a 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mods public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; - public virtual void ApplyToRulesetContainer(RulesetContainer rulesetContainer) => rulesetContainer.SetReplay(CreateReplayScore(rulesetContainer.Beatmap)?.Replay); + public virtual void ApplyToRulesetContainer(RulesetContainer rulesetContainer) => rulesetContainer.SetReplayScore(CreateReplayScore(rulesetContainer.Beatmap)); } public abstract class ModAutoplay : Mod, IApplicableFailOverride diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 0ebea9c2d0..4b3012192d 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -2,8 +2,11 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using osu.Framework.Configuration; +using osu.Framework.Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; @@ -57,6 +60,11 @@ namespace osu.Game.Rulesets.Scoring /// public readonly BindableInt Combo = new BindableInt(); + /// + /// Create a for this processor. + /// + protected virtual HitWindows CreateHitWindows() => new HitWindows(); + /// /// The current rank. /// @@ -166,7 +174,16 @@ namespace osu.Game.Rulesets.Scoring score.Accuracy = Math.Round(Accuracy, 4); score.Rank = Rank; score.Date = DateTimeOffset.Now; + + var hitWindows = CreateHitWindows(); + + foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) + score.Statistics[result] = GetStatistic(result); } + + protected abstract int GetStatistic(HitResult result); + + public abstract double GetStandardisedScore(); } public class ScoreProcessor : ScoreProcessor @@ -273,6 +290,8 @@ namespace osu.Game.Rulesets.Scoring updateScore(); } + private readonly Dictionary scoreResultCounts = new Dictionary(); + /// /// Applies the score change of a to this . /// @@ -284,6 +303,9 @@ namespace osu.Game.Rulesets.Scoring JudgedHits++; + if (result.Type != HitResult.None) + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; + if (result.Judgement.AffectsCombo) { switch (result.Type) @@ -340,20 +362,30 @@ namespace osu.Game.Rulesets.Scoring if (rollingMaxBaseScore != 0) Accuracy.Value = baseScore / rollingMaxBaseScore; - switch (Mode.Value) + TotalScore.Value = getScore(Mode.Value); + } + + private double getScore(ScoringMode mode) + { + switch (mode) { + default: case ScoringMode.Standardised: - TotalScore.Value = max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo / maxHighestCombo) + bonusScore; - break; + return max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo / maxHighestCombo) + bonusScore; case ScoringMode.Classic: // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) - TotalScore.Value = bonusScore + baseScore * (1 + Math.Max(0, HighestCombo - 1) / 25); - break; + return bonusScore + baseScore * (1 + Math.Max(0, HighestCombo - 1) / 25); } } + protected override int GetStatistic(HitResult result) => scoreResultCounts.GetOrDefault(result); + + public override double GetStandardisedScore() => getScore(ScoringMode.Standardised); + protected override void Reset(bool storeResults) { + scoreResultCounts.Clear(); + if (storeResults) { MaxHits = JudgedHits; diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 56222ff282..c8c7f3154b 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays; using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.UI { @@ -130,7 +131,7 @@ namespace osu.Game.Rulesets.UI protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null; - public Replay Replay { get; private set; } + public Score ReplayScore { get; private set; } /// /// Whether the game is paused. Used to block user input. @@ -140,14 +141,14 @@ namespace osu.Game.Rulesets.UI /// /// Sets a replay to be used, overriding local input. /// - /// The replay, null for local input. - public virtual void SetReplay(Replay replay) + /// The replay, null for local input. + public virtual void SetReplayScore(Score replayScore) { if (ReplayInputManager == null) throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports replay loading is not available"); - Replay = replay; - ReplayInputManager.ReplayInputHandler = replay != null ? CreateReplayInputHandler(replay) : null; + ReplayScore = replayScore; + ReplayInputManager.ReplayInputHandler = replayScore != null ? CreateReplayInputHandler(replayScore.Replay) : null; HasReplayLoaded.Value = ReplayInputManager.ReplayInputHandler != null; } @@ -302,9 +303,9 @@ namespace osu.Game.Rulesets.UI mod.ReadFromConfig(config); } - public override void SetReplay(Replay replay) + public override void SetReplayScore(Score replayScore) { - base.SetReplay(replay); + base.SetReplayScore(replayScore); if (ReplayInputManager?.ReplayInputHandler != null) ReplayInputManager.ReplayInputHandler.GamefieldToScreenSpace = Playfield.GamefieldToScreenSpace; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index fb894e621e..b863566967 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets; @@ -19,25 +20,39 @@ namespace osu.Game.Scoring { public int ID { get; set; } + [JsonProperty("rank")] + [JsonConverter(typeof(StringEnumConverter))] public ScoreRank Rank { get; set; } + [JsonProperty("total_score")] public int TotalScore { get; set; } + [JsonProperty("accuracy")] [Column(TypeName="DECIMAL(1,4)")] public double Accuracy { get; set; } + [JsonIgnore] public double? PP { get; set; } + [JsonProperty("max_combo")] public int MaxCombo { get; set; } - public int Combo { get; set; } + [JsonIgnore] + public int Combo { get; set; } // Todo: Shouldn't exist in here + [JsonIgnore] public int RulesetID { get; set; } + [JsonProperty("passed")] + [NotMapped] + public bool Passed { get; set; } = true; + + [JsonIgnore] public virtual RulesetInfo Ruleset { get; set; } private Mod[] mods; + [JsonProperty("mods")] [NotMapped] public Mod[] Mods { @@ -62,6 +77,7 @@ namespace osu.Game.Scoring private string modsJson; + [JsonIgnore] [Column("Mods")] public string ModsJson { @@ -84,9 +100,11 @@ namespace osu.Game.Scoring } } - [JsonIgnore] - public User User; + [NotMapped] + [JsonProperty("user")] + public User User { get; set; } + [JsonIgnore] [Column("User")] public string UserString { @@ -97,15 +115,19 @@ namespace osu.Game.Scoring [JsonIgnore] public int BeatmapInfoID { get; set; } + [JsonIgnore] public virtual BeatmapInfo Beatmap { get; set; } + [JsonIgnore] public long? OnlineScoreID { get; set; } + [JsonIgnore] public DateTimeOffset Date { get; set; } - [JsonIgnore] + [JsonProperty("statistics")] public Dictionary Statistics = new Dictionary(); + [JsonIgnore] [Column("Statistics")] public string StatisticsJson { @@ -125,8 +147,10 @@ namespace osu.Game.Scoring [JsonIgnore] public List Files { get; set; } + [JsonIgnore] public string Hash { get; set; } + [JsonIgnore] public bool DeletePending { get; set; } [Serializable] diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index ae1f27610b..14eae54cb2 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -17,7 +17,9 @@ using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Input; using osu.Game.Input.Bindings; +using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -90,7 +92,7 @@ namespace osu.Game.Screens.Menu buttonArea.Flow.CentreTarget = iconFacade; buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.fa_user, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.fa_users, new Color4(94, 63, 186, 255), () => OnMulti?.Invoke(), 0, Key.M)); + buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.fa_users, new Color4(94, 63, 186, 255), onMulti, 0, Key.M)); buttonsPlay.Add(new Button(@"chart", @"button-generic-select", FontAwesome.fa_osu_charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke())); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); @@ -103,19 +105,41 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(buttonsTopLevel); } - private OsuGame game; + [Resolved(CanBeNull = true)] + private OsuGame game { get; set; } + + [Resolved] + private APIAccess api { get; set; } + + [Resolved(CanBeNull = true)] + private NotificationOverlay notifications { get; set; } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, OsuGame game, IdleTracker idleTracker) + private void load(AudioManager audio, IdleTracker idleTracker) { - this.game = game; - isIdle.ValueChanged += updateIdleState; + if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); sampleBack = audio.Sample.Get(@"Menu/button-back-select"); } + private void onMulti() + { + if (!api.IsLoggedIn) + { + notifications?.Post(new SimpleNotification + { + Text = "You gotta be logged in to multi 'yo!", + Icon = FontAwesome.fa_globe + }); + + return; + } + + OnMulti?.Invoke(); + } + private void updateIdleState(bool isIdle) { if (isIdle && State != ButtonSystemState.Exit) diff --git a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs index 6dc59f5cac..b58af398ca 100644 --- a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs @@ -1,68 +1,89 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; +using osu.Framework.Configuration; 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; +using osu.Game.Online.Chat; namespace osu.Game.Screens.Multi.Components { - public class BeatmapTitle : FillFlowContainer + public class BeatmapTitle : CompositeDrawable { - private readonly OsuSpriteText beatmapTitle, beatmapDash, beatmapArtist; + public readonly IBindable Beatmap = new Bindable(); - public float TextSize - { - set { beatmapTitle.TextSize = beatmapDash.TextSize = beatmapArtist.TextSize = value; } - } - - private BeatmapInfo beatmap; - - public BeatmapInfo Beatmap - { - set - { - if (value == beatmap) return; - beatmap = value; - - if (IsLoaded) - updateText(); - } - } + private readonly LinkFlowContainer textFlow; public BeatmapTitle() { AutoSizeAxes = Axes.Both; - Direction = FillDirection.Horizontal; - Children = new[] - { - beatmapTitle = new OsuSpriteText { Font = @"Exo2.0-BoldItalic", }, - beatmapDash = new OsuSpriteText { Font = @"Exo2.0-BoldItalic", }, - beatmapArtist = new OsuSpriteText { Font = @"Exo2.0-RegularItalic", }, - }; + InternalChild = textFlow = new LinkFlowContainer { AutoSizeAxes = Axes.Both }; } protected override void LoadComplete() { base.LoadComplete(); - updateText(); + Beatmap.BindValueChanged(v => updateText(), true); } + private float textSize = OsuSpriteText.FONT_SIZE; + + public float TextSize + { + get => textSize; + set + { + if (textSize == value) + return; + textSize = value; + + updateText(); + } + } + + [Resolved] + private OsuColour colours { get; set; } + private void updateText() { - if (beatmap == null) - { - beatmapTitle.Text = "Changing map"; - beatmapDash.Text = beatmapArtist.Text = string.Empty; - } + if (!IsLoaded) + return; + + textFlow.Clear(); + + if (Beatmap.Value == null) + textFlow.AddText("No beatmap selected", s => + { + s.TextSize = TextSize; + s.Colour = colours.PinkLight; + }); else { - beatmapTitle.Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)); - beatmapDash.Text = @" - "; - beatmapArtist.Text = new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); + textFlow.AddLink(new[] + { + new OsuSpriteText + { + Text = new LocalisedString((Beatmap.Value.Metadata.ArtistUnicode, Beatmap.Value.Metadata.Artist)), + TextSize = TextSize, + }, + new OsuSpriteText + { + Text = " - ", + TextSize = TextSize, + }, + new OsuSpriteText + { + Text = new LocalisedString((Beatmap.Value.Metadata.TitleUnicode, Beatmap.Value.Metadata.Title)), + TextSize = TextSize, + } + }, null, LinkAction.OpenBeatmap, Beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap"); } } } diff --git a/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs b/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs index a22e171275..04ac0cc4c3 100644 --- a/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs +++ b/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs @@ -1,70 +1,77 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; using osuTK; namespace osu.Game.Screens.Multi.Components { - public class BeatmapTypeInfo : FillFlowContainer + public class BeatmapTypeInfo : CompositeDrawable { - private readonly ModeTypeInfo modeTypeInfo; - private readonly BeatmapTitle beatmapTitle; - private readonly OsuSpriteText beatmapAuthor; - - public BeatmapInfo Beatmap - { - set - { - modeTypeInfo.Beatmap = beatmapTitle.Beatmap = value; - beatmapAuthor.Text = value == null ? string.Empty : $"mapped by {value.Metadata.Author}"; - } - } - - public GameType Type - { - set { modeTypeInfo.Type = value; } - } + public readonly IBindable Beatmap = new Bindable(); + public readonly IBindable Ruleset = new Bindable(); + public readonly IBindable Type = new Bindable(); public BeatmapTypeInfo() { AutoSizeAxes = Axes.Both; - Direction = FillDirection.Horizontal; - LayoutDuration = 100; - Spacing = new Vector2(5f, 0f); - Children = new Drawable[] + BeatmapTitle beatmapTitle; + ModeTypeInfo modeTypeInfo; + LinkFlowContainer beatmapAuthor; + + InternalChild = new FillFlowContainer { - modeTypeInfo = new ModeTypeInfo(), - new Container + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + LayoutDuration = 100, + Spacing = new Vector2(5, 0), + Children = new Drawable[] { - AutoSizeAxes = Axes.X, - Height = 30, - Margin = new MarginPadding { Left = 5 }, - Children = new Drawable[] + modeTypeInfo = new ModeTypeInfo(), + new Container { - beatmapTitle = new BeatmapTitle(), - beatmapAuthor = new OsuSpriteText + AutoSizeAxes = Axes.X, + Height = 30, + Margin = new MarginPadding { Left = 5 }, + Children = new Drawable[] { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - TextSize = 14, + beatmapTitle = new BeatmapTitle(), + beatmapAuthor = new LinkFlowContainer(s => s.TextSize = 14) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both + }, }, }, - }, + } }; - } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - beatmapAuthor.Colour = colours.Gray9; + modeTypeInfo.Beatmap.BindTo(Beatmap); + modeTypeInfo.Ruleset.BindTo(Ruleset); + modeTypeInfo.Type.BindTo(Type); + + beatmapTitle.Beatmap.BindTo(Beatmap); + + Beatmap.BindValueChanged(v => + { + beatmapAuthor.Clear(); + + if (v != null) + { + beatmapAuthor.AddText("mapped by ", s => s.Colour = OsuColour.Gray(0.8f)); + beatmapAuthor.AddLink(v.Metadata.Author.Username, null, LinkAction.OpenUserProfile, v.Metadata.Author.Id.ToString(), "View Profile"); + } + }); } } } diff --git a/osu.Game/Screens/Multi/Components/DisableableTabControl.cs b/osu.Game/Screens/Multi/Components/DisableableTabControl.cs new file mode 100644 index 0000000000..dc765832db --- /dev/null +++ b/osu.Game/Screens/Multi/Components/DisableableTabControl.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; + +namespace osu.Game.Screens.Multi.Components +{ + public abstract class DisableableTabControl : TabControl + { + public readonly BindableBool Enabled = new BindableBool(); + + protected override void AddTabItem(TabItem tab, bool addToDropdown = true) + { + if (tab is DisableableTabItem disableable) + disableable.Enabled.BindTo(Enabled); + base.AddTabItem(tab, addToDropdown); + } + + protected abstract class DisableableTabItem : TabItem + { + public readonly BindableBool Enabled = new BindableBool(); + + protected DisableableTabItem(T value) + : base(value) + { + } + + protected override bool OnClick(ClickEvent e) + { + if (!Enabled) + return true; + return base.OnClick(e); + } + } + } +} diff --git a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs index 95f0c2153d..aad409e2c7 100644 --- a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs @@ -1,83 +1,67 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; using osuTK; namespace osu.Game.Screens.Multi.Components { - public class ModeTypeInfo : Container + public class ModeTypeInfo : CompositeDrawable { private const float height = 30; private const float transition_duration = 100; - private readonly Container rulesetContainer, gameTypeContainer; + private readonly Container rulesetContainer; - public BeatmapInfo Beatmap - { - set - { - if (value != null) - { - rulesetContainer.FadeIn(transition_duration); - rulesetContainer.Children = new[] - { - new DifficultyIcon(value) - { - Size = new Vector2(height), - }, - }; - } - else - { - rulesetContainer.FadeOut(transition_duration); - } - } - } - - public GameType Type - { - set - { - gameTypeContainer.Children = new[] - { - new DrawableGameType(value) - { - Size = new Vector2(height), - }, - }; - } - } + public readonly IBindable Beatmap = new Bindable(); + public readonly IBindable Ruleset = new Bindable(); + public readonly IBindable Type = new Bindable(); public ModeTypeInfo() { AutoSizeAxes = Axes.Both; - Children = new[] + Container gameTypeContainer; + + InternalChild = new FillFlowContainer { - new FillFlowContainer + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5f, 0f), + LayoutDuration = 100, + Children = new[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5f, 0f), - LayoutDuration = 100, - Children = new[] + rulesetContainer = new Container { - rulesetContainer = new Container - { - AutoSizeAxes = Axes.Both, - }, - gameTypeContainer = new Container - { - AutoSizeAxes = Axes.Both, - }, + AutoSizeAxes = Axes.Both, + }, + gameTypeContainer = new Container + { + AutoSizeAxes = Axes.Both, }, }, }; + + Beatmap.BindValueChanged(updateBeatmap); + Ruleset.BindValueChanged(_ => updateBeatmap(Beatmap.Value)); + Type.BindValueChanged(v => gameTypeContainer.Child = new DrawableGameType(v) { Size = new Vector2(height) }); + } + + private void updateBeatmap(BeatmapInfo beatmap) + { + if (beatmap != null) + { + rulesetContainer.FadeIn(transition_duration); + rulesetContainer.Child = new DifficultyIcon(beatmap, Ruleset.Value) { Size = new Vector2(height) }; + } + else + rulesetContainer.FadeOut(transition_duration); } } } diff --git a/osu.Game/Screens/Multi/Components/ParticipantCount.cs b/osu.Game/Screens/Multi/Components/ParticipantCount.cs index e7183cbd92..fc19039872 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantCount.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantCount.cs @@ -1,69 +1,67 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Users; namespace osu.Game.Screens.Multi.Components { - public class ParticipantCount : FillFlowContainer + public class ParticipantCountDisplay : CompositeDrawable { private const float text_size = 30; private const float transition_duration = 100; - private readonly OsuSpriteText count, slash, maxText; + private readonly OsuSpriteText slash, maxText; - public int Count - { - set => count.Text = value.ToString(); - } + public readonly IBindable> Participants = new Bindable>(); + public readonly IBindable ParticipantCount = new Bindable(); + public readonly IBindable MaxParticipants = new Bindable(); - private int? max; - public int? Max - { - get => max; - set - { - if (value == max) return; - max = value; - - updateMax(); - } - } - - public ParticipantCount() + public ParticipantCountDisplay() { AutoSizeAxes = Axes.Both; - Direction = FillDirection.Horizontal; - LayoutDuration = transition_duration; - Children = new[] + OsuSpriteText count; + + InternalChild = new FillFlowContainer { - count = new OsuSpriteText + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + LayoutDuration = transition_duration, + Children = new[] { - TextSize = text_size, - Font = @"Exo2.0-Bold" - }, - slash = new OsuSpriteText - { - Text = @"/", - TextSize = text_size, - Font = @"Exo2.0-Light" - }, - maxText = new OsuSpriteText - { - TextSize = text_size, - Font = @"Exo2.0-Light" - }, + count = new OsuSpriteText + { + TextSize = text_size, + Font = @"Exo2.0-Bold" + }, + slash = new OsuSpriteText + { + Text = @"/", + TextSize = text_size, + Font = @"Exo2.0-Light" + }, + maxText = new OsuSpriteText + { + TextSize = text_size, + Font = @"Exo2.0-Light" + }, + } }; - updateMax(); + Participants.BindValueChanged(v => count.Text = v.Count().ToString()); + MaxParticipants.BindValueChanged(_ => updateMax(), true); + ParticipantCount.BindValueChanged(v => count.Text = v.ToString("#,0")); } private void updateMax() { - if (Max == null) + if (MaxParticipants.Value == null) { slash.FadeOut(transition_duration); maxText.FadeOut(transition_duration); @@ -71,7 +69,7 @@ namespace osu.Game.Screens.Multi.Components else { slash.FadeIn(transition_duration); - maxText.Text = Max.ToString(); + maxText.Text = MaxParticipants.Value.ToString(); maxText.FadeIn(transition_duration); } } diff --git a/osu.Game/Screens/Multi/Components/ParticipantInfo.cs b/osu.Game/Screens/Multi/Components/ParticipantInfo.cs deleted file mode 100644 index b1c77a04af..0000000000 --- a/osu.Game/Screens/Multi/Components/ParticipantInfo.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -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.Users; -using osuTK; - -namespace osu.Game.Screens.Multi.Components -{ - public class ParticipantInfo : Container - { - private readonly Container flagContainer; - private readonly OsuSpriteText host; - private readonly FillFlowContainer levelRangeContainer; - private readonly OsuSpriteText levelRangeLower; - private readonly OsuSpriteText levelRangeHigher; - - public User Host - { - set - { - host.Text = value.Username; - flagContainer.Children = new[] { new DrawableFlag(value.Country) { RelativeSizeAxes = Axes.Both } }; - } - } - - public IEnumerable Participants - { - set - { - var ranks = value.Select(u => u.Statistics.Ranks.Global); - levelRangeLower.Text = ranks.Min().ToString(); - levelRangeHigher.Text = ranks.Max().ToString(); - } - } - - public ParticipantInfo(string rankPrefix = null) - { - RelativeSizeAxes = Axes.X; - Height = 15f; - - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5f, 0f), - Children = new Drawable[] - { - flagContainer = new Container - { - Width = 22f, - RelativeSizeAxes = Axes.Y, - }, - new Container //todo: team banners - { - Width = 38f, - RelativeSizeAxes = Axes.Y, - CornerRadius = 2f, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"ad387e"), - }, - }, - }, - new OsuSpriteText - { - Text = "hosted by", - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - TextSize = 14, - }, - host = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - TextSize = 14, - Font = @"Exo2.0-BoldItalic", - }, - }, - }, - levelRangeContainer = new FillFlowContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new[] - { - new OsuSpriteText - { - Text = rankPrefix, - TextSize = 14, - }, - new OsuSpriteText - { - Text = "#", - TextSize = 14, - }, - levelRangeLower = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-Bold", - }, - new OsuSpriteText - { - Text = " - ", - TextSize = 14, - }, - new OsuSpriteText - { - Text = "#", - TextSize = 14, - }, - levelRangeHigher = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-Bold", - }, - }, - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - levelRangeContainer.Colour = colours.Gray9; - host.Colour = colours.Blue; - } - } -} diff --git a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs b/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs new file mode 100644 index 0000000000..ca93ea2366 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs @@ -0,0 +1,111 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.RoomStatuses; + +namespace osu.Game.Screens.Multi.Components +{ + public class RoomStatusInfo : CompositeDrawable + { + private readonly RoomBindings bindings = new RoomBindings(); + + public RoomStatusInfo(Room room) + { + bindings.Room = room; + + AutoSizeAxes = Axes.Both; + + StatusPart statusPart; + EndDatePart endDatePart; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + statusPart = new StatusPart + { + TextSize = 14, + Font = "Exo2.0-Bold" + }, + endDatePart = new EndDatePart { TextSize = 14 } + } + }; + + statusPart.EndDate.BindTo(bindings.EndDate); + statusPart.Status.BindTo(bindings.Status); + statusPart.Availability.BindTo(bindings.Availability); + endDatePart.EndDate.BindTo(bindings.EndDate); + } + + private class EndDatePart : DrawableDate + { + public readonly IBindable EndDate = new Bindable(); + + public EndDatePart() + : base(DateTimeOffset.UtcNow) + { + EndDate.BindValueChanged(d => Date = d); + } + + protected override string Format() + { + var diffToNow = Date.Subtract(DateTimeOffset.Now); + + if (diffToNow.TotalSeconds < -5) + return $"Closed {base.Format()}"; + + if (diffToNow.TotalSeconds < 0) + return "Closed"; + + if (diffToNow.TotalSeconds < 5) + return "Closing soon"; + + return $"Closing {base.Format()}"; + } + } + + private class StatusPart : EndDatePart + { + public readonly IBindable Status = new Bindable(); + public readonly IBindable Availability = new Bindable(); + + [Resolved] + private OsuColour colours { get; set; } + + public StatusPart() + { + EndDate.BindValueChanged(_ => Format()); + Status.BindValueChanged(_ => Format()); + Availability.BindValueChanged(_ => Format()); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Text = Format(); + } + + protected override string Format() + { + if (!IsLoaded) + return string.Empty; + + RoomStatus status = Date < DateTimeOffset.Now ? new RoomStatusEnded() : Status.Value ?? new RoomStatusOpen(); + + this.FadeColour(status.GetAppropriateColour(colours), 100); + return $"{Availability.Value.GetDescription()}, {status.Message}"; + } + } + } +} diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 6bd300eadc..2849fd89e0 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -10,7 +10,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.SearchableList; -using osu.Game.Screens.Multi.Screens; using osuTK; using osuTK.Graphics; @@ -86,7 +85,12 @@ namespace osu.Game.Screens.Multi }, }; - breadcrumbs.Current.ValueChanged += s => screenType.Text = ((MultiplayerScreen)s).Type.ToLowerInvariant(); + breadcrumbs.Current.ValueChanged += s => + { + if (s is IMultiplayerSubScreen mpScreen) + screenType.Text = mpScreen.ShortTitle.ToLowerInvariant(); + }; + breadcrumbs.Current.TriggerChange(); } @@ -99,7 +103,8 @@ namespace osu.Game.Screens.Multi private class HeaderBreadcrumbControl : ScreenBreadcrumbControl { - public HeaderBreadcrumbControl(Screen initialScreen) : base(initialScreen) + public HeaderBreadcrumbControl(Screen initialScreen) + : base(initialScreen) { } diff --git a/osu.Game/Screens/Multi/IMultiplayerSubScreen.cs b/osu.Game/Screens/Multi/IMultiplayerSubScreen.cs new file mode 100644 index 0000000000..4796ffc05c --- /dev/null +++ b/osu.Game/Screens/Multi/IMultiplayerSubScreen.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Screens.Multi +{ + public interface IMultiplayerSubScreen + { + string ShortTitle { get; } + } +} diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/Multi/IRoomManager.cs new file mode 100644 index 0000000000..f0dbcb0e71 --- /dev/null +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Configuration; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Screens.Multi +{ + public interface IRoomManager + { + /// + /// Invoked when the s have been updated. + /// + event Action RoomsUpdated; + + /// + /// All the active s. + /// + IBindableCollection Rooms { get; } + + /// + /// Creates a new . + /// + /// The to create. + /// An action to be invoked if the creation succeeds. + /// An action to be invoked if an error occurred. + void CreateRoom(Room room, Action onSuccess = null, Action onError = null); + + /// + /// Joins a . + /// + /// The to join. must be populated. + /// + /// + void JoinRoom(Room room, Action onSuccess = null, Action onError = null); + + /// + /// Parts the currently-joined . + /// + void PartRoom(); + + /// + /// Queries for s matching a new . + /// + /// The to match. + void Filter(FilterCriteria criteria); + } +} diff --git a/osu.Game/Screens/Multi/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs similarity index 70% rename from osu.Game/Screens/Multi/Components/DrawableRoom.cs rename to osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index 05ec2f0fac..219121cb53 100644 --- a/osu.Game/Screens/Multi/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -5,12 +5,10 @@ using System; using System.Collections.Generic; using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -18,11 +16,11 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Users; +using osu.Game.Screens.Multi.Components; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.Multi.Lounge.Components { public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable { @@ -30,18 +28,21 @@ namespace osu.Game.Screens.Multi.Components private const float corner_radius = 5; private const float transition_duration = 60; private const float content_padding = 10; - private const float height = 100; + private const float height = 110; private const float side_strip_width = 5; private const float cover_width = 145; - private readonly Box selectionBox; + public event Action StateChanged; - private readonly Bindable nameBind = new Bindable(); - private readonly Bindable hostBind = new Bindable(); - private readonly Bindable statusBind = new Bindable(); - private readonly Bindable typeBind = new Bindable(); - private readonly Bindable beatmapBind = new Bindable(); - private readonly Bindable> participantsBind = new Bindable>(); + private readonly RoomBindings bindings = new RoomBindings(); + + private readonly Box selectionBox; + private UpdateableBeatmapBackgroundSprite background; + private BeatmapTitle beatmapTitle; + private ModeTypeInfo modeTypeInfo; + + [Resolved] + private BeatmapManager beatmaps { get; set; } public readonly Room Room; @@ -76,22 +77,10 @@ namespace osu.Game.Screens.Multi.Components } } - private Action action; - public new Action Action - { - get { return action; } - set - { - action = value; - Enabled.Value = action != null; - } - } - - public event Action StateChanged; - public DrawableRoom(Room room) { Room = room; + bindings.Room = room; RelativeSizeAxes = Axes.X; Height = height + SELECTION_BORDER_WIDTH * 2; @@ -110,11 +99,8 @@ namespace osu.Game.Screens.Multi.Components private void load(OsuColour colours) { Box sideStrip; - UpdateableBeatmapSetCover cover; - OsuSpriteText name, status; ParticipantInfo participantInfo; - BeatmapTitle beatmapTitle; - ModeTypeInfo modeTypeInfo; + OsuSpriteText name; Children = new Drawable[] { @@ -146,12 +132,13 @@ namespace osu.Game.Screens.Multi.Components RelativeSizeAxes = Axes.Y, Width = side_strip_width, }, - cover = new UpdateableBeatmapSetCover + new Container { - Width = cover_width, RelativeSizeAxes = Axes.Y, + Width = cover_width, Masking = true, Margin = new MarginPadding { Left = side_strip_width }, + Child = background = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both } }, new Container { @@ -172,10 +159,7 @@ namespace osu.Game.Screens.Multi.Components Spacing = new Vector2(5f), Children = new Drawable[] { - name = new OsuSpriteText - { - TextSize = 18, - }, + name = new OsuSpriteText { TextSize = 18 }, participantInfo = new ParticipantInfo(), }, }, @@ -186,18 +170,11 @@ namespace osu.Game.Screens.Multi.Components RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), Children = new Drawable[] { - status = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-Bold", - }, - beatmapTitle = new BeatmapTitle - { - TextSize = 14, - Colour = colours.Gray9 - }, + new RoomStatusInfo(Room), + beatmapTitle = new BeatmapTitle { TextSize = 14 }, }, }, modeTypeInfo = new ModeTypeInfo @@ -212,32 +189,21 @@ namespace osu.Game.Screens.Multi.Components }, }; - nameBind.ValueChanged += n => name.Text = n; - hostBind.ValueChanged += h => participantInfo.Host = h; - typeBind.ValueChanged += m => modeTypeInfo.Type = m; - participantsBind.ValueChanged += p => participantInfo.Participants = p; + background.Beatmap.BindTo(bindings.CurrentBeatmap); + modeTypeInfo.Beatmap.BindTo(bindings.CurrentBeatmap); + modeTypeInfo.Ruleset.BindTo(bindings.CurrentRuleset); + modeTypeInfo.Type.BindTo(bindings.Type); + beatmapTitle.Beatmap.BindTo(bindings.CurrentBeatmap); + participantInfo.Host.BindTo(bindings.Host); + participantInfo.Participants.BindTo(bindings.Participants); + participantInfo.ParticipantCount.BindTo(bindings.ParticipantCount); - statusBind.ValueChanged += s => + bindings.Name.BindValueChanged(n => name.Text = n, true); + bindings.Status.BindValueChanged(s => { - status.Text = s.Message; - - foreach (Drawable d in new Drawable[] { selectionBox, sideStrip, status }) + foreach (Drawable d in new Drawable[] { selectionBox, sideStrip }) d.FadeColour(s.GetAppropriateColour(colours), transition_duration); - }; - - beatmapBind.ValueChanged += b => - { - cover.BeatmapSet = b?.BeatmapSet; - beatmapTitle.Beatmap = b; - modeTypeInfo.Beatmap = b; - }; - - nameBind.BindTo(Room.Name); - hostBind.BindTo(Room.Host); - statusBind.BindTo(Room.Status); - typeBind.BindTo(Room.Type); - beatmapBind.BindTo(Room.Beatmap); - participantsBind.BindTo(Room.Participants); + }, true); } protected override void LoadComplete() @@ -245,16 +211,5 @@ namespace osu.Game.Screens.Multi.Components base.LoadComplete(); this.FadeInFromZero(transition_duration); } - - protected override bool OnClick(ClickEvent e) - { - if (Enabled.Value) - { - Action?.Invoke(this); - State = SelectionState.Selected; - } - - return true; - } } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs new file mode 100644 index 0000000000..286a4c18b0 --- /dev/null +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; +using osu.Game.Graphics; +using osu.Game.Overlays.SearchableList; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi.Lounge.Components +{ + public class FilterControl : SearchableListFilterControl + { + protected override Color4 BackgroundColour => OsuColour.FromHex(@"362e42"); + protected override PrimaryFilter DefaultTab => PrimaryFilter.Open; + + public FilterControl() + { + DisplayStyleControl.Hide(); + } + + public FilterCriteria CreateCriteria() => new FilterCriteria + { + SearchString = Search.Current.Value ?? string.Empty, + PrimaryFilter = Tabs.Current, + SecondaryFilter = DisplayStyleControl.Dropdown.Current + }; + } + + public enum PrimaryFilter + { + Open, + [Description("Recently Ended")] + RecentlyEnded, + Participated, + Owned, + } + + public enum SecondaryFilter + { + Public, + //Private, + } +} diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs new file mode 100644 index 0000000000..0a1c8a3e84 --- /dev/null +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Screens.Multi.Lounge.Components +{ + public class FilterCriteria + { + public string SearchString; + public PrimaryFilter PrimaryFilter; + public SecondaryFilter SecondaryFilter; + } +} diff --git a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs new file mode 100644 index 0000000000..34fc7fe886 --- /dev/null +++ b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +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.Users; +using osuTK; + +namespace osu.Game.Screens.Multi.Lounge.Components +{ + public class ParticipantInfo : Container + { + private readonly FillFlowContainer summaryContainer; + + public readonly IBindable Host = new Bindable(); + public readonly IBindable> Participants = new Bindable>(); + public readonly IBindable ParticipantCount = new Bindable(); + + public ParticipantInfo() + { + OsuSpriteText summary; + RelativeSizeAxes = Axes.X; + Height = 15f; + + OsuSpriteText levelRangeHigher; + OsuSpriteText levelRangeLower; + Container flagContainer; + LinkFlowContainer hostText; + + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5f, 0f), + Children = new Drawable[] + { + flagContainer = new Container + { + Width = 22f, + RelativeSizeAxes = Axes.Y, + }, + /*new Container //todo: team banners + { + Width = 38f, + RelativeSizeAxes = Axes.Y, + CornerRadius = 2f, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"ad387e"), + }, + }, + },*/ + hostText = new LinkFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both + } + }, + }, + summaryContainer = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + summary = new OsuSpriteText + { + Text = "0 participants", + TextSize = 14, + } + }, + }, + }; + + Host.BindValueChanged(v => + { + hostText.Clear(); + hostText.AddText("hosted by "); + hostText.AddLink(v.Username, null, LinkAction.OpenUserProfile, v.Id.ToString(), "Open profile", s => s.Font = "Exo2.0-BoldItalic"); + + flagContainer.Child = new DrawableFlag(v.Country) { RelativeSizeAxes = Axes.Both }; + }); + + ParticipantCount.BindValueChanged(v => summary.Text = $"{v:#,0}{" participant".Pluralize(v == 1)}"); + + /*Participants.BindValueChanged(v => + { + var ranks = v.Select(u => u.Statistics.Ranks.Global); + levelRangeLower.Text = ranks.Min().ToString(); + levelRangeHigher.Text = ranks.Max().ToString(); + });*/ + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + summaryContainer.Colour = colours.Gray9; + } + } +} diff --git a/osu.Game/Screens/Multi/Components/RoomInspector.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs similarity index 74% rename from osu.Game/Screens/Multi/Components/RoomInspector.cs rename to osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs index 1ec509d5a8..47f5182c39 100644 --- a/osu.Game/Screens/Multi/Components/RoomInspector.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -17,66 +16,35 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Components; using osu.Game.Users; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.Multi.Lounge.Components { public class RoomInspector : Container { private const float transition_duration = 100; + public readonly IBindable Room = new Bindable(); + private readonly MarginPadding contentPadding = new MarginPadding { Horizontal = 20, Vertical = 10 }; - private readonly Bindable nameBind = new Bindable(); - private readonly Bindable hostBind = new Bindable(); - private readonly Bindable statusBind = new Bindable(); - private readonly Bindable typeBind = new Bindable(); - private readonly Bindable beatmapBind = new Bindable(); - private readonly Bindable maxParticipantsBind = new Bindable(); - private readonly Bindable> participantsBind = new Bindable>(); + + private readonly RoomBindings bindings = new RoomBindings(); private OsuColour colours; private Box statusStrip; - private UpdateableBeatmapSetCover cover; - private ParticipantCount participantCount; + private UpdateableBeatmapBackgroundSprite background; + private ParticipantCountDisplay participantCount; private FillFlowContainer topFlow, participantsFlow; private OsuSpriteText name, status; private BeatmapTypeInfo beatmapTypeInfo; private ScrollContainer participantsScroll; private ParticipantInfo participantInfo; - private Room room; - public Room Room - { - get { return room; } - set - { - if (value == room) return; - room = value; - - nameBind.UnbindBindings(); - hostBind.UnbindBindings(); - statusBind.UnbindBindings(); - typeBind.UnbindBindings(); - beatmapBind.UnbindBindings(); - maxParticipantsBind.UnbindBindings(); - participantsBind.UnbindBindings(); - - if (room != null) - { - nameBind.BindTo(room.Name); - hostBind.BindTo(room.Host); - statusBind.BindTo(room.Status); - typeBind.BindTo(room.Type); - beatmapBind.BindTo(room.Beatmap); - maxParticipantsBind.BindTo(room.MaxParticipants); - participantsBind.BindTo(room.Participants); - } - - updateState(); - } - } + [Resolved] + private BeatmapManager beatmaps { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -104,10 +72,7 @@ namespace osu.Game.Screens.Multi.Components Masking = true, Children = new Drawable[] { - cover = new UpdateableBeatmapSetCover - { - RelativeSizeAxes = Axes.Both, - }, + background = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }, new Box { RelativeSizeAxes = Axes.Both, @@ -119,7 +84,7 @@ namespace osu.Game.Screens.Multi.Components Padding = new MarginPadding(20), Children = new Drawable[] { - participantCount = new ParticipantCount + participantCount = new ParticipantCountDisplay { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -177,7 +142,7 @@ namespace osu.Game.Screens.Multi.Components Padding = contentPadding, Children = new Drawable[] { - participantInfo = new ParticipantInfo(@"Rank Range "), + participantInfo = new ParticipantInfo(), }, }, }, @@ -201,26 +166,48 @@ namespace osu.Game.Screens.Multi.Components }, }; - nameBind.ValueChanged += n => name.Text = n; - hostBind.ValueChanged += h => participantInfo.Host = h; - typeBind.ValueChanged += t => beatmapTypeInfo.Type = t; - maxParticipantsBind.ValueChanged += m => participantCount.Max = m; - statusBind.ValueChanged += displayStatus; + participantInfo.Host.BindTo(bindings.Host); + participantInfo.ParticipantCount.BindTo(bindings.ParticipantCount); + participantInfo.Participants.BindTo(bindings.Participants); - beatmapBind.ValueChanged += b => + participantCount.Participants.BindTo(bindings.Participants); + participantCount.ParticipantCount.BindTo(bindings.ParticipantCount); + participantCount.MaxParticipants.BindTo(bindings.MaxParticipants); + + beatmapTypeInfo.Beatmap.BindTo(bindings.CurrentBeatmap); + beatmapTypeInfo.Ruleset.BindTo(bindings.CurrentRuleset); + beatmapTypeInfo.Type.BindTo(bindings.Type); + background.Beatmap.BindTo(bindings.CurrentBeatmap); + + bindings.Status.BindValueChanged(displayStatus); + bindings.Participants.BindValueChanged(p => participantsFlow.ChildrenEnumerable = p.Select(u => new UserTile(u))); + bindings.Name.BindValueChanged(n => name.Text = n); + + Room.BindValueChanged(updateRoom, true); + } + + private void updateRoom(Room room) + { + bindings.Room = room; + + if (room != null) { - cover.BeatmapSet = b?.BeatmapSet; - beatmapTypeInfo.Beatmap = b; - }; - - participantsBind.ValueChanged += p => + participantsFlow.FadeIn(transition_duration); + participantCount.FadeIn(transition_duration); + beatmapTypeInfo.FadeIn(transition_duration); + name.FadeIn(transition_duration); + participantInfo.FadeIn(transition_duration); + } + else { - participantCount.Count = p.Count(); - participantInfo.Participants = p; - participantsFlow.ChildrenEnumerable = p.Select(u => new UserTile(u)); - }; + participantsFlow.FadeOut(transition_duration); + participantCount.FadeOut(transition_duration); + beatmapTypeInfo.FadeOut(transition_duration); + name.FadeOut(transition_duration); + participantInfo.FadeOut(transition_duration); - updateState(); + displayStatus(new RoomStatusNoneSelected()); + } } protected override void UpdateAfterChildren() @@ -239,32 +226,6 @@ namespace osu.Game.Screens.Multi.Components status.FadeColour(c, transition_duration); } - private void updateState() - { - if (Room == null) - { - cover.BeatmapSet = null; - participantsFlow.FadeOut(transition_duration); - participantCount.FadeOut(transition_duration); - beatmapTypeInfo.FadeOut(transition_duration); - name.FadeOut(transition_duration); - participantInfo.FadeOut(transition_duration); - - displayStatus(new RoomStatusNoneSelected()); - } - else - { - participantsFlow.FadeIn(transition_duration); - participantCount.FadeIn(transition_duration); - beatmapTypeInfo.FadeIn(transition_duration); - name.FadeIn(transition_duration); - participantInfo.FadeIn(transition_duration); - - statusBind.TriggerChange(); - beatmapBind.TriggerChange(); - } - } - private class UserTile : Container, IHasTooltip { private readonly User user; diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs new file mode 100644 index 0000000000..5133e96a52 --- /dev/null +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -0,0 +1,135 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.Multi.Lounge.Components +{ + public class RoomsContainer : CompositeDrawable + { + public Action JoinRequested; + + private readonly Bindable selectedRoom = new Bindable(); + public IBindable SelectedRoom => selectedRoom; + + private readonly IBindableCollection rooms = new BindableCollection(); + + private readonly FillFlowContainer roomFlow; + public IReadOnlyList Rooms => roomFlow; + + [Resolved] + private IRoomManager roomManager { get; set; } + + public RoomsContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = roomFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + }; + } + + [BackgroundDependencyLoader] + private void load() + { + rooms.BindTo(roomManager.Rooms); + + rooms.ItemsAdded += addRooms; + rooms.ItemsRemoved += removeRooms; + + roomManager.RoomsUpdated += updateSorting; + + addRooms(rooms); + } + + private FilterCriteria currentFilter; + + public void Filter(FilterCriteria criteria) + { + roomFlow.Children.ForEach(r => + { + if (criteria == null) + r.MatchingFilter = true; + else + { + bool matchingFilter = true; + matchingFilter &= r.FilterTerms.Any(term => term.IndexOf(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase) >= 0); + + switch (criteria.SecondaryFilter) + { + default: + case SecondaryFilter.Public: + r.MatchingFilter = r.Room.Availability.Value == RoomAvailability.Public; + break; + } + + r.MatchingFilter = matchingFilter; + } + }); + + currentFilter = criteria; + } + + private void addRooms(IEnumerable rooms) + { + foreach (var r in rooms) + roomFlow.Add(new DrawableRoom(r) { Action = () => selectRoom(r) }); + + Filter(currentFilter); + } + + private void removeRooms(IEnumerable rooms) + { + foreach (var r in rooms) + { + var toRemove = roomFlow.Single(d => d.Room == r); + toRemove.Action = null; + + roomFlow.Remove(toRemove); + + selectRoom(null); + } + } + + private void updateSorting() + { + foreach (var room in roomFlow) + roomFlow.SetLayoutPosition(room, room.Room.Position); + } + + private void selectRoom(Room room) + { + var drawable = roomFlow.FirstOrDefault(r => r.Room == room); + + if (drawable != null && drawable.State == SelectionState.Selected) + JoinRequested?.Invoke(room); + else + roomFlow.Children.ForEach(r => r.State = r.Room == room ? SelectionState.Selected : SelectionState.NotSelected); + + selectedRoom.Value = room; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (roomManager != null) + roomManager.RoomsUpdated -= updateSorting; + } + } +} diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs new file mode 100644 index 0000000000..d9633218eb --- /dev/null +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -0,0 +1,151 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.SearchableList; +using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.Match; + +namespace osu.Game.Screens.Multi.Lounge +{ + public class LoungeSubScreen : MultiplayerSubScreen + { + protected readonly FilterControl Filter; + + private readonly Container content; + private readonly RoomsContainer rooms; + private readonly Action pushGameplayScreen; + private readonly ProcessingOverlay processingOverlay; + + [Resolved(CanBeNull = true)] + private IRoomManager roomManager { get; set; } + + public override string Title => "Lounge"; + + protected override Drawable TransitionContent => content; + + public LoungeSubScreen(Action pushGameplayScreen) + { + this.pushGameplayScreen = pushGameplayScreen; + + RoomInspector inspector; + + Children = new Drawable[] + { + Filter = new FilterControl { Depth = -1 }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Width = 0.55f, + Children = new Drawable[] + { + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Padding = new MarginPadding(10), + Child = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = rooms = new RoomsContainer { JoinRequested = joinRequested } + }, + }, + processingOverlay = new ProcessingOverlay { Alpha = 0 } + } + }, + inspector = new RoomInspector + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = 0.45f, + }, + }, + }, + }; + + inspector.Room.BindTo(rooms.SelectedRoom); + + Filter.Search.Current.ValueChanged += s => filterRooms(); + Filter.Tabs.Current.ValueChanged += t => filterRooms(); + Filter.Search.Exit += Exit; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + content.Padding = new MarginPadding + { + Top = Filter.DrawHeight, + Left = SearchableListOverlay.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH, + Right = SearchableListOverlay.WIDTH_PADDING, + }; + } + + protected override void OnFocus(FocusEvent e) + { + GetContainingInputManager().ChangeFocus(Filter.Search); + } + + protected override void OnEntering(Screen last) + { + base.OnEntering(last); + Filter.Search.HoldFocus = true; + } + + protected override bool OnExiting(Screen next) + { + Filter.Search.HoldFocus = false; + // no base call; don't animate + return false; + } + + protected override void OnSuspending(Screen next) + { + base.OnSuspending(next); + Filter.Search.HoldFocus = false; + } + + private void filterRooms() + { + rooms.Filter(Filter.CreateCriteria()); + roomManager?.Filter(Filter.CreateCriteria()); + } + + private void joinRequested(Room room) + { + processingOverlay.Show(); + roomManager?.JoinRoom(room, r => + { + Push(room); + processingOverlay.Hide(); + }, _ => processingOverlay.Hide()); + } + + /// + /// Push a room as a new subscreen. + /// + public void Push(Room room) + { + // Handles the case where a room is clicked 3 times in quick succession + if (!IsCurrentScreen) + return; + + Push(new MatchSubScreen(room, s => pushGameplayScreen?.Invoke(s))); + } + } +} diff --git a/osu.Game/Screens/Multi/Screens/Match/Settings/GameTypePicker.cs b/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs similarity index 90% rename from osu.Game/Screens/Multi/Screens/Match/Settings/GameTypePicker.cs rename to osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs index cd8b081b4e..caf686165f 100644 --- a/osu.Game/Screens/Multi/Screens/Match/Settings/GameTypePicker.cs +++ b/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs @@ -9,17 +9,19 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.GameTypes; using osu.Game.Screens.Multi.Components; using osuTK; -namespace osu.Game.Screens.Multi.Screens.Match.Settings +namespace osu.Game.Screens.Multi.Match.Components { - public class GameTypePicker : TabControl + public class GameTypePicker : DisableableTabControl { private const float height = 40; private const float selection_width = 3; protected override TabItem CreateTabItem(GameType value) => new GameTypePickerItem(value); + protected override Dropdown CreateDropdown() => null; public GameTypePicker() @@ -31,15 +33,17 @@ namespace osu.Game.Screens.Multi.Screens.Match.Settings AddItem(new GameTypeVersus()); AddItem(new GameTypeTagTeam()); AddItem(new GameTypeTeamVersus()); + AddItem(new GameTypeTimeshift()); } - private class GameTypePickerItem : TabItem + private class GameTypePickerItem : DisableableTabItem { private const float transition_duration = 200; private readonly CircularContainer hover, selection; - public GameTypePickerItem(GameType value) : base(value) + public GameTypePickerItem(GameType value) + : base(value) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs new file mode 100644 index 0000000000..4cb6d7a4e0 --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/Header.cs @@ -0,0 +1,147 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +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.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.SearchableList; +using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.Play.HUD; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class Header : Container + { + public const float HEIGHT = 200; + + private readonly RoomBindings bindings = new RoomBindings(); + + private readonly Box tabStrip; + + public readonly MatchTabControl Tabs; + + public Action OnRequestSelectBeatmap; + + public Header(Room room) + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + bindings.Room = room; + + BeatmapTypeInfo beatmapTypeInfo; + BeatmapSelectButton beatmapButton; + UpdateableBeatmapBackgroundSprite background; + ModDisplay modDisplay; + + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + background = new HeaderBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.4f), Color4.Black.Opacity(0.6f)), + }, + } + }, + tabStrip = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = 1, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 20 }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + beatmapTypeInfo = new BeatmapTypeInfo(), + modDisplay = new ModDisplay + { + Scale = new Vector2(0.75f), + DisplayUnrankedText = false + }, + } + }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = 200, + Padding = new MarginPadding { Vertical = 10 }, + Child = beatmapButton = new BeatmapSelectButton(room) + { + RelativeSizeAxes = Axes.Both, + Height = 1, + }, + }, + Tabs = new MatchTabControl(room) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X + }, + }, + }, + }; + + beatmapTypeInfo.Beatmap.BindTo(bindings.CurrentBeatmap); + beatmapTypeInfo.Ruleset.BindTo(bindings.CurrentRuleset); + beatmapTypeInfo.Type.BindTo(bindings.Type); + background.Beatmap.BindTo(bindings.CurrentBeatmap); + bindings.CurrentMods.BindValueChanged(m => modDisplay.Current.Value = m, true); + + beatmapButton.Action = () => OnRequestSelectBeatmap?.Invoke(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + tabStrip.Colour = colours.Yellow; + } + + private class BeatmapSelectButton : HeaderButton + { + private readonly IBindable roomIDBind = new Bindable(); + + public BeatmapSelectButton(Room room) + { + Text = "Select beatmap"; + + roomIDBind.BindTo(room.RoomID); + roomIDBind.BindValueChanged(v => this.FadeTo(v.HasValue ? 0 : 1), true); + } + } + + private class HeaderBeatmapBackgroundSprite : UpdateableBeatmapBackgroundSprite + { + protected override double FadeDuration => 200; + } + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/HeaderButton.cs b/osu.Game/Screens/Multi/Match/Components/HeaderButton.cs new file mode 100644 index 0000000000..30fe609ede --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/HeaderButton.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +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.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class HeaderButton : TriangleButton + { + [BackgroundDependencyLoader] + private void load() + { + BackgroundColour = OsuColour.FromHex(@"1187aa"); + + Triangles.ColourLight = OsuColour.FromHex(@"277b9c"); + Triangles.ColourDark = OsuColour.FromHex(@"1f6682"); + Triangles.TriangleScale = 1.5f; + + Add(new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 1f, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.15f, + Blending = BlendingMode.Additive, + }, + }); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = @"Exo2.0-Light", + TextSize = 30, + }; + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/HostInfo.cs b/osu.Game/Screens/Multi/Match/Components/HostInfo.cs new file mode 100644 index 0000000000..993dccce44 --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/HostInfo.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Chat; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class HostInfo : CompositeDrawable + { + public readonly IBindable Host = new Bindable(); + + private readonly LinkFlowContainer linkContainer; + private readonly UpdateableAvatar avatar; + + public HostInfo() + { + AutoSizeAxes = Axes.X; + Height = 50; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + avatar = new UpdateableAvatar { Size = new Vector2(50) }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Child = linkContainer = new LinkFlowContainer { AutoSizeAxes = Axes.Both } + } + } + }; + + Host.BindValueChanged(updateHost); + } + + private void updateHost(User host) + { + avatar.User = host; + + if (host != null) + { + linkContainer.AddText("hosted by"); + linkContainer.NewLine(); + linkContainer.AddLink(host.Username, null, LinkAction.OpenUserProfile, host.Id.ToString(), "View Profile", s => s.Font = "Exo2.0-BoldItalic"); + } + } + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/Info.cs b/osu.Game/Screens/Multi/Match/Components/Info.cs new file mode 100644 index 0000000000..0aabc014c9 --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/Info.cs @@ -0,0 +1,100 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +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.Multiplayer; +using osu.Game.Overlays.SearchableList; +using osu.Game.Screens.Multi.Components; +using osuTK; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class Info : Container + { + public Action OnStart; + + private readonly RoomBindings bindings = new RoomBindings(); + + public Info(Room room) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + ReadyButton readyButton; + ViewBeatmapButton viewBeatmapButton; + HostInfo hostInfo; + RoomStatusInfo statusInfo; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"28242d"), + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Padding = new MarginPadding { Vertical = 20 }, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + TextSize = 30, + Current = bindings.Name + }, + new RoomStatusInfo(room), + } + }, + hostInfo = new HostInfo(), + }, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.X, + Height = 70, + Spacing = new Vector2(10, 0), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + viewBeatmapButton = new ViewBeatmapButton(), + readyButton = new ReadyButton(room) + { + Action = () => OnStart?.Invoke() + } + } + } + }, + }, + }; + + viewBeatmapButton.Beatmap.BindTo(bindings.CurrentBeatmap); + readyButton.Beatmap.BindTo(bindings.CurrentBeatmap); + hostInfo.Host.BindTo(bindings.Host); + + bindings.Room = room; + } + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs new file mode 100644 index 0000000000..0d7221754f --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Game.Online.Chat; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class MatchChatDisplay : StandAloneChatDisplay + { + private readonly Room room; + + [Resolved(CanBeNull = true)] + private ChannelManager channelManager { get; set; } + + public MatchChatDisplay(Room room) + : base(true) + { + this.room = room; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + room.RoomID.BindValueChanged(v => updateChannel(), true); + } + + private void updateChannel() + { + if (room.RoomID.Value != null) + Channel.Value = channelManager?.JoinChannel(new Channel { Id = room.ChannelId, Type = ChannelType.Multiplayer, Name = $"#mp_{room.RoomID}" }); + } + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs new file mode 100644 index 0000000000..864191105f --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class MatchLeaderboard : Leaderboard + { + public Action> ScoresLoaded; + + private readonly Room room; + + public MatchLeaderboard(Room room) + { + this.room = room; + } + + [BackgroundDependencyLoader] + private void load() + { + room.RoomID.BindValueChanged(id => + { + if (id == null) + return; + + Scores = null; + UpdateScores(); + }, true); + } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + if (room.RoomID == null) + return null; + + var req = new GetRoomScoresRequest(room.RoomID.Value ?? 0); + + req.Success += r => + { + scoresCallback?.Invoke(r); + ScoresLoaded?.Invoke(r); + }; + + return req; + } + + protected override LeaderboardScore CreateDrawableScore(APIRoomScoreInfo model, int index) => new MatchLeaderboardScore(model, index); + } + + public enum MatchLeaderboardScope + { + Overall + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs new file mode 100644 index 0000000000..ebd69b24fc --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class MatchLeaderboardScore : LeaderboardScore + { + public MatchLeaderboardScore(APIRoomScoreInfo score, int rank) + : base(score, rank) + { + } + + [BackgroundDependencyLoader] + private void load() + { + RankContainer.Alpha = 0; + } + + protected override IEnumerable GetStatistics(ScoreInfo model) => new[] + { + new LeaderboardScoreStatistic(FontAwesome.fa_crosshairs, "Accuracy", string.Format(model.Accuracy % 1 == 0 ? @"{0:P0}" : @"{0:P2}", model.Accuracy)), + new LeaderboardScoreStatistic(FontAwesome.fa_refresh, "Total Attempts", ((APIRoomScoreInfo)model).TotalAttempts.ToString()), + new LeaderboardScoreStatistic(FontAwesome.fa_check, "Completed Beatmaps", ((APIRoomScoreInfo)model).CompletedBeatmaps.ToString()), + }; + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchPage.cs b/osu.Game/Screens/Multi/Match/Components/MatchPage.cs new file mode 100644 index 0000000000..54ba345934 --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/MatchPage.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public abstract class MatchPage + { + public abstract string Name { get; } + + public readonly BindableBool Enabled = new BindableBool(true); + + public override string ToString() => Name; + public override int GetHashCode() => GetType().GetHashCode(); + public override bool Equals(object obj) => GetType() == obj?.GetType(); + } + + public class SettingsMatchPage : MatchPage + { + public override string Name => "Settings"; + } + + public class RoomMatchPage : MatchPage + { + public override string Name => "Room"; + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs new file mode 100644 index 0000000000..69ca4b1deb --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -0,0 +1,406 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using Humanizer; +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; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.SearchableList; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class MatchSettingsOverlay : FocusedOverlayContainer + { + private const float transition_duration = 350; + private const float field_padding = 45; + private const float disabled_alpha = 0.2f; + + private readonly RoomBindings bindings = new RoomBindings(); + + private readonly Container content; + + private readonly OsuSpriteText typeLabel; + + protected readonly OsuTextBox NameField, MaxParticipantsField; + protected readonly OsuDropdown DurationField; + protected readonly RoomAvailabilityPicker AvailabilityPicker; + protected readonly GameTypePicker TypePicker; + protected readonly TriangleButton ApplyButton; + protected readonly OsuPasswordTextBox PasswordField; + + protected readonly OsuSpriteText ErrorText; + + private readonly ProcessingOverlay processingOverlay; + + private readonly Room room; + + [Resolved(CanBeNull = true)] + private IRoomManager manager { get; set; } + + public MatchSettingsOverlay(Room room) + { + this.room = room; + + bindings.Room = room; + + Masking = true; + + Child = content = new Container + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"28242d"), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new ScrollContainer + { + Padding = new MarginPadding { Vertical = 10 }, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new SectionContainer + { + Padding = new MarginPadding { Right = field_padding / 2 }, + Children = new[] + { + new Section("Room name") + { + Child = NameField = new SettingsTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + OnCommit = (sender, text) => apply(), + }, + }, + new Section("Room visibility") + { + Alpha = disabled_alpha, + Child = AvailabilityPicker = new RoomAvailabilityPicker(), + }, + new Section("Game type") + { + Alpha = disabled_alpha, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(7), + Children = new Drawable[] + { + TypePicker = new GameTypePicker + { + RelativeSizeAxes = Axes.X, + }, + typeLabel = new OsuSpriteText + { + TextSize = 14, + }, + }, + }, + }, + }, + }, + new SectionContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding { Left = field_padding / 2 }, + Children = new[] + { + new Section("Max participants") + { + Alpha = disabled_alpha, + Child = MaxParticipantsField = new SettingsNumberTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + OnCommit = (sender, text) => apply(), + }, + }, + new Section("Duration") + { + Child = DurationField = new DurationDropdown + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + TimeSpan.FromMinutes(30), + TimeSpan.FromHours(1), + TimeSpan.FromHours(2), + TimeSpan.FromHours(4), + TimeSpan.FromHours(8), + TimeSpan.FromHours(12), + //TimeSpan.FromHours(16), + TimeSpan.FromHours(24), + TimeSpan.FromDays(3), + TimeSpan.FromDays(7) + } + } + }, + new Section("Password (optional)") + { + Alpha = disabled_alpha, + Child = PasswordField = new SettingsPasswordTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + OnCommit = (sender, text) => apply() + }, + }, + }, + }, + }, + } + }, + }, + }, + new Drawable[] + { + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = 2, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding { Vertical = 20 }, + Children = new Drawable[] + { + ApplyButton = new CreateRoomButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(230, 55), + Action = apply, + }, + ErrorText = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Alpha = 0, + Depth = 1 + } + } + } + } + } + } + } + }, + processingOverlay = new ProcessingOverlay { Alpha = 0 } + }, + }; + + TypePicker.Current.ValueChanged += t => typeLabel.Text = t.Name; + + bindings.Name.BindValueChanged(n => NameField.Text = n, true); + bindings.Availability.BindValueChanged(a => AvailabilityPicker.Current.Value = a, true); + bindings.Type.BindValueChanged(t => TypePicker.Current.Value = t, true); + bindings.MaxParticipants.BindValueChanged(m => MaxParticipantsField.Text = m?.ToString(), true); + bindings.Duration.BindValueChanged(d => DurationField.Current.Value = d, true); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + typeLabel.Colour = colours.Yellow; + ErrorText.Colour = colours.RedDark; + + MaxParticipantsField.ReadOnly = true; + PasswordField.ReadOnly = true; + AvailabilityPicker.Enabled.Value = false; + TypePicker.Enabled.Value = false; + ApplyButton.Enabled.Value = false; + } + + protected override void Update() + { + base.Update(); + + ApplyButton.Enabled.Value = hasValidSettings; + } + + private bool hasValidSettings => bindings.Room.RoomID.Value == null && NameField.Text.Length > 0 && bindings.Playlist.Count > 0; + + protected override void PopIn() + { + content.MoveToY(0, transition_duration, Easing.OutQuint); + } + + protected override void PopOut() + { + content.MoveToY(-1, transition_duration, Easing.InSine); + } + + private void apply() + { + hideError(); + + bindings.Name.Value = NameField.Text; + bindings.Availability.Value = AvailabilityPicker.Current.Value; + bindings.Type.Value = TypePicker.Current.Value; + + if (int.TryParse(MaxParticipantsField.Text, out int max)) + bindings.MaxParticipants.Value = max; + else + bindings.MaxParticipants.Value = null; + + bindings.Duration.Value = DurationField.Current.Value; + + manager?.CreateRoom(room, onSuccess, onError); + + processingOverlay.Show(); + } + + private void hideError() => ErrorText.FadeOut(50); + + private void onSuccess(Room room) => processingOverlay.Hide(); + + private void onError(string text) + { + ErrorText.Text = text; + ErrorText.FadeIn(50); + + processingOverlay.Hide(); + } + + private class SettingsTextBox : OsuTextBox + { + protected override Color4 BackgroundUnfocused => Color4.Black; + protected override Color4 BackgroundFocused => Color4.Black; + } + + private class SettingsNumberTextBox : SettingsTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } + + private class SettingsPasswordTextBox : OsuPasswordTextBox + { + protected override Color4 BackgroundUnfocused => Color4.Black; + protected override Color4 BackgroundFocused => Color4.Black; + } + + private class SectionContainer : FillFlowContainer
+ { + public SectionContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Width = 0.5f; + Direction = FillDirection.Vertical; + Spacing = new Vector2(field_padding); + } + } + + private class Section : Container + { + private readonly Container content; + + protected override Container Content => content; + + public Section(string title) + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + TextSize = 12, + Font = @"Exo2.0-Bold", + Text = title.ToUpper(), + }, + content = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }, + }, + }; + } + } + + private class CreateRoomButton : TriangleButton + { + public CreateRoomButton() + { + Text = "Create"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Yellow; + Triangles.ColourLight = colours.YellowLight; + Triangles.ColourDark = colours.YellowDark; + } + } + + private class DurationDropdown : OsuDropdown + { + public DurationDropdown() + { + Menu.MaxHeight = 100; + } + + protected override string GenerateItemText(TimeSpan item) + { + return item.Humanize(); + } + } + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchTabControl.cs b/osu.Game/Screens/Multi/Match/Components/MatchTabControl.cs new file mode 100644 index 0000000000..a9932ee3c6 --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/MatchTabControl.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class MatchTabControl : PageTabControl + { + private readonly IBindable roomIdBind = new Bindable(); + + public MatchTabControl(Room room) + { + roomIdBind.BindTo(room.RoomID); + + AddItem(new RoomMatchPage()); + AddItem(new SettingsMatchPage()); + } + + [BackgroundDependencyLoader] + private void load() + { + roomIdBind.BindValueChanged(v => + { + if (v.HasValue) + { + Items.ForEach(t => t.Enabled.Value = !(t is SettingsMatchPage)); + Current.Value = new RoomMatchPage(); + } + else + { + Items.ForEach(t => t.Enabled.Value = t is SettingsMatchPage); + Current.Value = new SettingsMatchPage(); + } + }, true); + } + + protected override TabItem CreateTabItem(MatchPage value) => new TabItem(value); + + private class TabItem : PageTabItem + { + private readonly IBindable enabled = new BindableBool(); + + public TabItem(MatchPage value) + : base(value) + { + enabled.BindTo(value.Enabled); + enabled.BindValueChanged(v => Colour = v ? Color4.White : Color4.Gray); + } + + protected override bool OnClick(ClickEvent e) + { + if (!enabled.Value) + return true; + return base.OnClick(e); + } + } + } +} diff --git a/osu.Game/Screens/Multi/Screens/Match/Participants.cs b/osu.Game/Screens/Multi/Match/Components/Participants.cs similarity index 68% rename from osu.Game/Screens/Multi/Screens/Match/Participants.cs rename to osu.Game/Screens/Multi/Match/Components/Participants.cs index 55541a1acd..56c8c3e8fc 100644 --- a/osu.Game/Screens/Multi/Screens/Match/Participants.cs +++ b/osu.Game/Screens/Multi/Match/Components/Participants.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.SearchableList; @@ -10,36 +11,20 @@ using osu.Game.Screens.Multi.Components; using osu.Game.Users; using osuTK; -namespace osu.Game.Screens.Multi.Screens.Match +namespace osu.Game.Screens.Multi.Match.Components { - public class Participants : Container + public class Participants : CompositeDrawable { - private readonly ParticipantCount count; - private readonly FillFlowContainer usersFlow; - - public IEnumerable Users - { - set { - usersFlow.Children = value.Select(u => new UserPanel(u) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Width = 300, - OnLoadComplete = d => d.FadeInFromZero(60), - }).ToList(); - - count.Count = value.Count(); - } - } - - public int? Max - { - set => count.Max = value; - } + public readonly IBindable> Users = new Bindable>(); + public readonly IBindable ParticipantCount = new Bindable(); + public readonly IBindable MaxParticipants = new Bindable(); public Participants() { - Child = new Container + FillFlowContainer usersFlow; + ParticipantCountDisplay count; + + InternalChild = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, @@ -51,7 +36,7 @@ namespace osu.Game.Screens.Multi.Screens.Match Padding = new MarginPadding { Top = 10 }, Children = new Drawable[] { - count = new ParticipantCount + count = new ParticipantCountDisplay { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -69,6 +54,21 @@ namespace osu.Game.Screens.Multi.Screens.Match }, }, }; + + count.Participants.BindTo(Users); + count.ParticipantCount.BindTo(ParticipantCount); + count.MaxParticipants.BindTo(MaxParticipants); + + Users.BindValueChanged(v => + { + usersFlow.Children = v.Select(u => new UserPanel(u) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 300, + OnLoadComplete = d => d.FadeInFromZero(60), + }).ToList(); + }); } } } diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs new file mode 100644 index 0000000000..d3726da246 --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class ReadyButton : HeaderButton + { + public readonly IBindable Beatmap = new Bindable(); + + private readonly Room room; + + [Resolved] + private IBindableBeatmap gameBeatmap { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + private bool hasBeatmap; + + public ReadyButton(Room room) + { + this.room = room; + RelativeSizeAxes = Axes.Y; + Size = new Vector2(200, 1); + + Text = "Start"; + } + + [BackgroundDependencyLoader] + private void load() + { + beatmaps.ItemAdded += beatmapAdded; + + Beatmap.BindValueChanged(updateBeatmap, true); + } + + private void updateBeatmap(BeatmapInfo beatmap) + { + hasBeatmap = false; + + if (beatmap?.OnlineBeatmapID == null) + return; + + hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID) != null; + } + + private void beatmapAdded(BeatmapSetInfo model, bool existing, bool silent) + { + if (model.Beatmaps.Any(b => b.OnlineBeatmapID == Beatmap.Value.OnlineBeatmapID)) + Schedule(() => hasBeatmap = true); + } + + protected override void Update() + { + base.Update(); + + updateEnabledState(); + } + + private void updateEnabledState() + { + if (gameBeatmap.Value == null) + { + Enabled.Value = false; + return; + } + + bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < room.EndDate; + + Enabled.Value = hasBeatmap && hasEnoughTime; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmaps != null) + beatmaps.ItemAdded -= beatmapAdded; + } + } +} diff --git a/osu.Game/Screens/Multi/Screens/Match/Settings/RoomAvailabilityPicker.cs b/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs similarity index 91% rename from osu.Game/Screens/Multi/Screens/Match/Settings/RoomAvailabilityPicker.cs rename to osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs index 251bd062ec..4ddc9b30e9 100644 --- a/osu.Game/Screens/Multi/Screens/Match/Settings/RoomAvailabilityPicker.cs +++ b/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs @@ -10,12 +10,13 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Components; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Screens.Match.Settings +namespace osu.Game.Screens.Multi.Match.Components { - public class RoomAvailabilityPicker : TabControl + public class RoomAvailabilityPicker : DisableableTabControl { protected override TabItem CreateTabItem(RoomAvailability value) => new RoomAvailabilityPickerItem(value); protected override Dropdown CreateDropdown() => null; @@ -32,7 +33,7 @@ namespace osu.Game.Screens.Multi.Screens.Match.Settings AddItem(RoomAvailability.InviteOnly); } - private class RoomAvailabilityPickerItem : TabItem + private class RoomAvailabilityPickerItem : DisableableTabItem { private const float transition_duration = 200; @@ -41,7 +42,7 @@ namespace osu.Game.Screens.Multi.Screens.Match.Settings public RoomAvailabilityPickerItem(RoomAvailability value) : base(value) { RelativeSizeAxes = Axes.Y; - Width = 120; + Width = 102; Masking = true; CornerRadius = 5; diff --git a/osu.Game/Screens/Multi/Match/Components/ViewBeatmapButton.cs b/osu.Game/Screens/Multi/Match/Components/ViewBeatmapButton.cs new file mode 100644 index 0000000000..82d0761fbf --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/ViewBeatmapButton.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osuTK; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class ViewBeatmapButton : HeaderButton + { + public readonly IBindable Beatmap = new Bindable(); + + [Resolved(CanBeNull = true)] + private OsuGame osuGame { get; set; } + + public ViewBeatmapButton() + { + RelativeSizeAxes = Axes.Y; + Size = new Vector2(200, 1); + + Text = "View beatmap"; + } + + [BackgroundDependencyLoader] + private void load() + { + if (osuGame != null) + Beatmap.BindValueChanged(updateAction, true); + } + + private void updateAction(BeatmapInfo beatmap) + { + if (beatmap == null) + { + Enabled.Value = false; + return; + } + + Action = () => osuGame.ShowBeatmap(beatmap.OnlineBeatmapID ?? 0); + Enabled.Value = true; + } + } +} diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs new file mode 100644 index 0000000000..55a5a2c85e --- /dev/null +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -0,0 +1,203 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.GameTypes; +using osu.Game.Rulesets; +using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.Multi.Play; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select; + +namespace osu.Game.Screens.Multi.Match +{ + public class MatchSubScreen : MultiplayerSubScreen + { + public override bool AllowBeatmapRulesetChange => false; + public override string Title => room.RoomID.Value == null ? "New room" : room.Name.Value; + public override string ShortTitle => "room"; + + private readonly RoomBindings bindings = new RoomBindings(); + + private readonly MatchLeaderboard leaderboard; + + private readonly Action pushGameplayScreen; + + [Cached] + private readonly Room room; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + [Resolved(CanBeNull = true)] + private OsuGame game { get; set; } + + [Resolved(CanBeNull = true)] + private IRoomManager manager { get; set; } + + public MatchSubScreen(Room room, Action pushGameplayScreen) + { + this.room = room; + this.pushGameplayScreen = pushGameplayScreen; + + bindings.Room = room; + + MatchChatDisplay chat; + Components.Header header; + MatchSettingsOverlay settings; + + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { header = new Components.Header(room) { Depth = -1 } }, + new Drawable[] { new Info(room) { OnStart = onStart } }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + leaderboard = new MatchLeaderboard(room) + { + Padding = new MarginPadding(10), + RelativeSizeAxes = Axes.Both + }, + new Container + { + Padding = new MarginPadding(10), + RelativeSizeAxes = Axes.Both, + Child = chat = new MatchChatDisplay(room) + { + RelativeSizeAxes = Axes.Both + } + }, + }, + }, + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Distributed), + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = Components.Header.HEIGHT }, + Child = settings = new MatchSettingsOverlay(room) { RelativeSizeAxes = Axes.Both }, + }, + }; + + header.OnRequestSelectBeatmap = () => Push(new MatchSongSelect { Selected = addPlaylistItem }); + header.Tabs.Current.ValueChanged += t => + { + if (t is SettingsMatchPage) + settings.Show(); + else + settings.Hide(); + }; + + chat.Exit += Exit; + } + + [BackgroundDependencyLoader] + private void load() + { + beatmapManager.ItemAdded += beatmapAdded; + } + + protected override bool OnExiting(Screen next) + { + manager?.PartRoom(); + return base.OnExiting(next); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + bindings.CurrentBeatmap.BindValueChanged(setBeatmap, true); + bindings.CurrentRuleset.BindValueChanged(setRuleset, true); + } + + private void setBeatmap(BeatmapInfo beatmap) + { + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID); + + game?.ForcefullySetBeatmap(beatmapManager.GetWorkingBeatmap(localBeatmap)); + } + + private void setRuleset(RulesetInfo ruleset) + { + if (ruleset == null) + return; + + game?.ForcefullySetRuleset(ruleset); + } + + private void beatmapAdded(BeatmapSetInfo model, bool existing, bool silent) => Schedule(() => + { + if (Beatmap.Value != beatmapManager.DefaultBeatmap) + return; + + if (bindings.CurrentBeatmap.Value == null) + return; + + // Try to retrieve the corresponding local beatmap + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == bindings.CurrentBeatmap.Value.OnlineBeatmapID); + + if (localBeatmap != null) + game?.ForcefullySetBeatmap(beatmapManager.GetWorkingBeatmap(localBeatmap)); + }); + + private void addPlaylistItem(PlaylistItem item) + { + bindings.Playlist.Clear(); + bindings.Playlist.Add(item); + } + + private void onStart() + { + Beatmap.Value.Mods.Value = bindings.CurrentMods.Value.ToArray(); + + switch (bindings.Type.Value) + { + default: + case GameTypeTimeshift _: + pushGameplayScreen?.Invoke(new PlayerLoader(() => { + var player = new TimeshiftPlayer(room, room.Playlist.First().ID); + player.Exited += _ => leaderboard.RefreshScores(); + + return player; + })); + break; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmapManager != null) + beatmapManager.ItemAdded -= beatmapAdded; + } + } +} diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 7822fa68dc..ce0eddbee3 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -1,33 +1,54 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Screens.Menu; -using osu.Game.Screens.Multi.Screens.Lounge; +using osu.Game.Screens.Multi.Lounge; +using osu.Game.Screens.Multi.Match; +using osuTK; namespace osu.Game.Screens.Multi { - public class Multiplayer : OsuScreen + [Cached] + public class Multiplayer : OsuScreen, IOnlineComponent { private readonly MultiplayerWaveContainer waves; - protected override Container Content => waves; + public override bool AllowBeatmapRulesetChange => currentSubScreen?.AllowBeatmapRulesetChange ?? base.AllowBeatmapRulesetChange; + + private readonly OsuButton createButton; + private readonly LoungeSubScreen loungeSubScreen; + + private OsuScreen currentSubScreen; + + [Cached(Type = typeof(IRoomManager))] + private RoomManager roomManager; + + [Resolved] + private APIAccess api { get; set; } public Multiplayer() { - InternalChild = waves = new MultiplayerWaveContainer + Child = waves = new MultiplayerWaveContainer { RelativeSizeAxes = Axes.Both, }; - Lounge lounge; - Children = new Drawable[] + waves.AddRange(new Drawable[] { new Container { @@ -53,16 +74,78 @@ namespace osu.Game.Screens.Multi { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = Header.HEIGHT }, - Child = lounge = new Lounge(), + Child = loungeSubScreen = new LoungeSubScreen(Push), }, - new Header(lounge), - }; + new Header(loungeSubScreen), + createButton = new HeaderButton + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.None, + Size = new Vector2(150, Header.HEIGHT - 20), + Margin = new MarginPadding + { + Top = 10, + Right = 10, + }, + Text = "Create room", + Action = () => loungeSubScreen.Push(new Room + { + Name = { Value = $"{api.LocalUser}'s awesome room" } + }), + }, + roomManager = new RoomManager() + }); - lounge.Exited += s => Exit(); + screenAdded(loungeSubScreen); + loungeSubScreen.Exited += _ => Exit(); + } + + private readonly IBindable isIdle = new BindableBool(); + + [BackgroundDependencyLoader(true)] + private void load(IdleTracker idleTracker) + { + api.Register(this); + + if (idleTracker != null) + isIdle.BindTo(idleTracker.IsIdle); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + isIdle.BindValueChanged(updatePollingRate, true); + } + + private void updatePollingRate(bool idle) + { + roomManager.TimeBetweenPolls = !IsCurrentScreen || !(currentSubScreen is LoungeSubScreen) ? 0 : (idle ? 120000 : 15000); + Logger.Log($"Polling adjusted to {roomManager.TimeBetweenPolls}"); + } + + public void APIStateChanged(APIAccess api, APIState state) + { + if (state != APIState.Online) + forcefullyExit(); + } + + private void forcefullyExit() + { + // This is temporary since we don't currently have a way to force screens to be exited + if (IsCurrentScreen) + Exit(); + else + { + MakeCurrent(); + Schedule(forcefullyExit); + } } protected override void OnEntering(Screen last) { + Content.FadeIn(); + base.OnEntering(last); waves.Show(); } @@ -70,19 +153,42 @@ namespace osu.Game.Screens.Multi protected override bool OnExiting(Screen next) { waves.Hide(); + + Content.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); + + cancelLooping(); + loungeSubScreen.MakeCurrent(); + updatePollingRate(isIdle.Value); + return base.OnExiting(next); } protected override void OnResuming(Screen last) { base.OnResuming(last); - waves.Show(); + + Content.FadeIn(250); + Content.ScaleTo(1, 250, Easing.OutSine); + + updatePollingRate(isIdle.Value); } protected override void OnSuspending(Screen next) { + Content.ScaleTo(1.1f, 250, Easing.InSine); + Content.FadeOut(250); + + cancelLooping(); + roomManager.TimeBetweenPolls = 0; + base.OnSuspending(next); - waves.Hide(); + } + + private void cancelLooping() + { + var track = Beatmap.Value.Track; + if (track != null) + track.Looping = false; } protected override void LogoExiting(OsuLogo logo) @@ -92,6 +198,57 @@ namespace osu.Game.Screens.Multi base.LogoExiting(logo); } + protected override void Update() + { + base.Update(); + + if (!IsCurrentScreen) return; + + if (currentSubScreen is MatchSubScreen) + { + var track = Beatmap.Value.Track; + if (track != null) + { + track.Looping = true; + + if (!track.IsRunning) + { + Game.Audio.AddItemToList(track); + track.Seek(Beatmap.Value.Metadata.PreviewTime); + track.Start(); + } + } + + createButton.Hide(); + } + else if (currentSubScreen is LoungeSubScreen) + createButton.Show(); + } + + private void screenAdded(Screen newScreen) + { + currentSubScreen = (OsuScreen)newScreen; + updatePollingRate(isIdle.Value); + + newScreen.ModePushed += screenAdded; + newScreen.Exited += screenRemoved; + } + + private void screenRemoved(Screen newScreen) + { + if (currentSubScreen is MatchSubScreen) + cancelLooping(); + + currentSubScreen = (OsuScreen)newScreen; + updatePollingRate(isIdle.Value); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + api?.Unregister(this); + } + private class MultiplayerWaveContainer : WaveContainer { protected override bool StartHidden => true; diff --git a/osu.Game/Screens/Multi/Screens/MultiplayerScreen.cs b/osu.Game/Screens/Multi/MultiplayerSubScreen.cs similarity index 80% rename from osu.Game/Screens/Multi/Screens/MultiplayerScreen.cs rename to osu.Game/Screens/Multi/MultiplayerSubScreen.cs index 00c2613d54..5a7eaafba5 100644 --- a/osu.Game/Screens/Multi/Screens/MultiplayerScreen.cs +++ b/osu.Game/Screens/Multi/MultiplayerSubScreen.cs @@ -2,20 +2,16 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Graphics.Containers; -namespace osu.Game.Screens.Multi.Screens +namespace osu.Game.Screens.Multi { - public abstract class MultiplayerScreen : OsuScreen + public abstract class MultiplayerSubScreen : OsuScreen, IMultiplayerSubScreen { - protected virtual Container TransitionContent => Content; + protected virtual Drawable TransitionContent => Content; - /// - /// The type to display in the title of the . - /// - public virtual string Type => Title; + public virtual string ShortTitle => Title; protected override void OnEntering(Screen last) { diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs new file mode 100644 index 0000000000..84d0ca3621 --- /dev/null +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Diagnostics; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Logging; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Multiplayer; +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 +{ + public class TimeshiftPlayer : Player + { + private readonly Room room; + private readonly int playlistItemId; + + [Resolved] + private APIAccess api { get; set; } + + public TimeshiftPlayer(Room room, int playlistItemId) + { + this.room = room; + this.playlistItemId = playlistItemId; + } + + private int? token; + + [BackgroundDependencyLoader] + private void load() + { + token = null; + + bool failed = false; + + var req = new CreateRoomScoreRequest(room.RoomID.Value ?? 0, playlistItemId); + req.Success += r => token = r.ID; + req.Failure += e => + { + failed = true; + + Logger.Error(e, "Failed to retrieve a score submission token."); + + Schedule(() => + { + ValidForResume = false; + Exit(); + }); + }; + + api.Queue(req); + + while (!failed && !token.HasValue) + Thread.Sleep(1000); + } + + protected override ScoreInfo CreateScore() + { + submitScore(); + return base.CreateScore(); + } + + private void submitScore() + { + var score = base.CreateScore(); + + score.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); + + Debug.Assert(token != null); + + var request = new SubmitRoomScoreRequest(token.Value, room.RoomID.Value ?? 0, playlistItemId, score); + request.Failure += e => Logger.Error(e, "Failed to submit score"); + api.Queue(request); + } + + protected override Results CreateResults(ScoreInfo score) => new MatchResults(score, room); + } +} diff --git a/osu.Game/Screens/Multi/Ranking/MatchResults.cs b/osu.Game/Screens/Multi/Ranking/MatchResults.cs new file mode 100644 index 0000000000..018eb60564 --- /dev/null +++ b/osu.Game/Screens/Multi/Ranking/MatchResults.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Online.Multiplayer; +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 + { + private readonly Room room; + + public MatchResults(ScoreInfo score, Room room) + : base(score) + { + this.room = room; + } + + protected override IEnumerable CreateResultPages() => new IResultPageInfo[] + { + new ScoreOverviewPageInfo(Score, Beatmap), + new LocalLeaderboardPageInfo(Score, Beatmap), + new RoomLeaderboardPageInfo(Score, Beatmap, room), + }; + } +} diff --git a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs new file mode 100644 index 0000000000..54528e5503 --- /dev/null +++ b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs @@ -0,0 +1,140 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Internal; +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.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 + { + private readonly Room room; + + private OsuColour colours; + + private TextFlowContainer rankText; + + public RoomLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap, Room room) + : base(score, beatmap) + { + this.room = room; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + this.colours = colours; + + MatchLeaderboard leaderboard; + + Children = new Drawable[] + { + new Box + { + Colour = colours.Gray6, + RelativeSizeAxes = Axes.Both, + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + BackgroundColour = colours.Gray6, + Child = leaderboard = CreateLeaderboard(room) + }, + 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) + { + Action gray = s => s.Colour = colours.GrayC; + Action white = s => + { + s.TextSize *= 1.4f; + s.Colour = colours.GrayF; + }; + + rankText.AddText(room.Name + "\n", white); + rankText.AddText("You are placed ", gray); + + int index = scores.IndexOf(new APIRoomScoreInfo { User = Score.User }, new FuncEqualityComparer((s1, s2) => s1.User.Id.Equals(s2.User.Id))); + + rankText.AddText($"#{index + 1} ", s => + { + s.Font = "Exo2.0-Bold"; + s.Colour = colours.YellowDark; + }); + + rankText.AddText("in the room!", gray); + } + + protected virtual MatchLeaderboard CreateLeaderboard(Room room) => new ResultsMatchLeaderboard(room); + + public class ResultsMatchLeaderboard : MatchLeaderboard + { + public ResultsMatchLeaderboard(Room room) + : base(room) + { + } + + protected override bool FadeTop => true; + + protected override LeaderboardScore CreateDrawableScore(APIRoomScoreInfo 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(APIRoomScoreInfo 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 new file mode 100644 index 0000000000..bc78210484 --- /dev/null +++ b/osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; +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; + private readonly Room room; + + public RoomLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap, Room room) + { + this.score = score; + this.beatmap = beatmap; + this.room = room; + } + + public FontAwesome Icon => FontAwesome.fa_users; + + public string Name => "Room Leaderboard"; + + public virtual ResultsPage CreatePage() => new RoomLeaderboardPage(score, beatmap, room); + } +} diff --git a/osu.Game/Screens/Multi/RoomBindings.cs b/osu.Game/Screens/Multi/RoomBindings.cs new file mode 100644 index 0000000000..dc2547268d --- /dev/null +++ b/osu.Game/Screens/Multi/RoomBindings.cs @@ -0,0 +1,106 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Configuration; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Users; + +namespace osu.Game.Screens.Multi +{ + /// + /// Helper class which binds to values from a . + /// + public class RoomBindings + { + public RoomBindings() + { + Playlist.ItemsAdded += _ => updatePlaylist(); + Playlist.ItemsRemoved += _ => updatePlaylist(); + } + + private Room room; + + /// + /// The to bind to. + /// + public Room Room + { + get => room; + set + { + if (room == value) + return; + + if (room != null) + { + Name.UnbindFrom(room.Name); + Host.UnbindFrom(room.Host); + Status.UnbindFrom(room.Status); + Type.UnbindFrom(room.Type); + Playlist.UnbindFrom(room.Playlist); + Participants.UnbindFrom(room.Participants); + ParticipantCount.UnbindFrom(room.ParticipantCount); + MaxParticipants.UnbindFrom(room.MaxParticipants); + EndDate.UnbindFrom(room.EndDate); + Availability.UnbindFrom(room.Availability); + Duration.UnbindFrom(room.Duration); + } + + room = value; + + if (room != null) + { + Name.BindTo(room.Name); + Host.BindTo(room.Host); + Status.BindTo(room.Status); + Type.BindTo(room.Type); + Playlist.BindTo(room.Playlist); + Participants.BindTo(room.Participants); + ParticipantCount.BindTo(room.ParticipantCount); + MaxParticipants.BindTo(room.MaxParticipants); + EndDate.BindTo(room.EndDate); + Availability.BindTo(room.Availability); + Duration.BindTo(room.Duration); + } + } + } + + private void updatePlaylist() + { + // Todo: We only ever have one playlist item for now. In the future, this will be user-settable + + var playlistItem = Playlist.FirstOrDefault(); + + currentBeatmap.Value = playlistItem?.Beatmap; + currentMods.Value = playlistItem?.RequiredMods ?? Enumerable.Empty(); + currentRuleset.Value = playlistItem?.Ruleset; + } + + public readonly Bindable Name = new Bindable(); + public readonly Bindable Host = new Bindable(); + public readonly Bindable Status = new Bindable(); + public readonly Bindable Type = new Bindable(); + public readonly BindableCollection Playlist = new BindableCollection(); + public readonly Bindable> Participants = new Bindable>(); + public readonly Bindable ParticipantCount = new Bindable(); + public readonly Bindable MaxParticipants = new Bindable(); + public readonly Bindable EndDate = new Bindable(); + public readonly Bindable Availability = new Bindable(); + public readonly Bindable Duration = new Bindable(); + + private readonly Bindable currentBeatmap = new Bindable(); + public IBindable CurrentBeatmap => currentBeatmap; + + private readonly Bindable> currentMods = new Bindable>(); + public IBindable> CurrentMods => currentMods; + + private readonly Bindable currentRuleset = new Bindable(); + public IBindable CurrentRuleset => currentRuleset; + } +} diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs new file mode 100644 index 0000000000..fab19c3fd7 --- /dev/null +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -0,0 +1,179 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Screens.Multi +{ + public class RoomManager : PollingComponent, IRoomManager + { + public event Action RoomsUpdated; + + private readonly BindableCollection rooms = new BindableCollection(); + public IBindableCollection Rooms => rooms; + + private Room currentRoom; + + private FilterCriteria currentFilter = new FilterCriteria(); + + [Resolved] + private APIAccess api { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + PartRoom(); + } + + public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + { + room.Host.Value = api.LocalUser; + + var req = new CreateRoomRequest(room); + + req.Success += result => + { + update(room, result); + addRoom(room); + + RoomsUpdated?.Invoke(); + + onSuccess?.Invoke(room); + }; + + req.Failure += exception => + { + if (req.Result != null) + onError?.Invoke(req.Result.Error); + else + Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important); + }; + + api.Queue(req); + } + + private JoinRoomRequest currentJoinRoomRequest; + + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + { + currentJoinRoomRequest?.Cancel(); + currentJoinRoomRequest = null; + + currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value); + currentJoinRoomRequest.Success += () => + { + currentRoom = room; + onSuccess?.Invoke(room); + }; + + currentJoinRoomRequest.Failure += exception => + { + Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); + onError?.Invoke(exception.ToString()); + }; + + api.Queue(currentJoinRoomRequest); + } + + public void PartRoom() + { + if (currentRoom == null) + return; + + api.Queue(new PartRoomRequest(currentRoom, api.LocalUser.Value)); + currentRoom = null; + } + + public void Filter(FilterCriteria criteria) + { + currentFilter = criteria; + PollImmediately(); + } + + private GetRoomsRequest pollReq; + + protected override Task Poll() + { + if (!api.IsLoggedIn) + return base.Poll(); + + var tcs = new TaskCompletionSource(); + + pollReq?.Cancel(); + pollReq = new GetRoomsRequest(currentFilter.PrimaryFilter); + + pollReq.Success += result => + { + // Remove past matches + foreach (var r in rooms.ToList()) + { + if (result.All(e => e.RoomID.Value != r.RoomID.Value)) + rooms.Remove(r); + } + + for (int i = 0; i < result.Count; i++) + { + var r = result[i]; + r.Position = i; + + update(r, r); + addRoom(r); + } + + RoomsUpdated?.Invoke(); + + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + api.Queue(pollReq); + + return tcs.Task; + } + + /// + /// Updates a local with a remote copy. + /// + /// The local to update. + /// The remote to update with. + private void update(Room local, Room remote) + { + foreach (var pi in remote.Playlist) + pi.MapObjects(beatmaps, rulesets); + + local.CopyFrom(remote); + } + + /// + /// Adds a to the list of available rooms. + /// + /// The to add.< + private void addRoom(Room room) + { + var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); + if (existing == null) + rooms.Add(room); + else + existing.CopyFrom(room); + } + } +} diff --git a/osu.Game/Screens/Multi/Screens/Lounge/FilterControl.cs b/osu.Game/Screens/Multi/Screens/Lounge/FilterControl.cs deleted file mode 100644 index 69cb6ad349..0000000000 --- a/osu.Game/Screens/Multi/Screens/Lounge/FilterControl.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.SearchableList; -using osuTK.Graphics; - -namespace osu.Game.Screens.Multi.Screens.Lounge -{ - public class FilterControl : SearchableListFilterControl - { - protected override Color4 BackgroundColour => OsuColour.FromHex(@"362e42"); - protected override LoungeTab DefaultTab => LoungeTab.Public; - - public FilterControl() - { - DisplayStyleControl.Hide(); - } - } - - public enum LoungeTab - { - Public = RoomAvailability.Public, - Private = RoomAvailability.FriendsOnly, - } -} diff --git a/osu.Game/Screens/Multi/Screens/Lounge/Lounge.cs b/osu.Game/Screens/Multi/Screens/Lounge/Lounge.cs deleted file mode 100644 index 0af941a668..0000000000 --- a/osu.Game/Screens/Multi/Screens/Lounge/Lounge.cs +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Framework.Screens; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.SearchableList; -using osu.Game.Screens.Multi.Components; -using osuTK; - -namespace osu.Game.Screens.Multi.Screens.Lounge -{ - public class Lounge : MultiplayerScreen - { - private readonly Container content; - private readonly SearchContainer search; - - protected readonly FilterControl Filter; - protected readonly FillFlowContainer RoomsContainer; - protected readonly RoomInspector Inspector; - - public override string Title => "Lounge"; - - protected override Container TransitionContent => content; - - private IEnumerable rooms; - public IEnumerable Rooms - { - get { return rooms; } - set - { - if (Equals(value, rooms)) return; - rooms = value; - - var enumerable = rooms.ToList(); - - RoomsContainer.Children = enumerable.Select(r => new DrawableRoom(r) - { - Action = didSelect, - }).ToList(); - - if (!enumerable.Contains(Inspector.Room)) - Inspector.Room = null; - - filterRooms(); - } - } - - public Lounge() - { - Children = new Drawable[] - { - Filter = new FilterControl(), - content = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new ScrollContainer - { - RelativeSizeAxes = Axes.Both, - Width = 0.55f, - Padding = new MarginPadding - { - Vertical = 35 - DrawableRoom.SELECTION_BORDER_WIDTH, - Right = 20 - DrawableRoom.SELECTION_BORDER_WIDTH - }, - Child = search = new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = RoomsContainer = new RoomsFilterContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10 - DrawableRoom.SELECTION_BORDER_WIDTH * 2), - }, - }, - }, - Inspector = new RoomInspector - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - Width = 0.45f, - }, - }, - }, - }; - - Filter.Search.Current.ValueChanged += s => filterRooms(); - Filter.Tabs.Current.ValueChanged += t => filterRooms(); - Filter.Search.Exit += Exit; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - content.Padding = new MarginPadding - { - Top = Filter.DrawHeight, - Left = SearchableListOverlay.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH, - Right = SearchableListOverlay.WIDTH_PADDING, - }; - } - - protected override void OnFocus(FocusEvent e) - { - GetContainingInputManager().ChangeFocus(Filter.Search); - } - - protected override void OnEntering(Screen last) - { - base.OnEntering(last); - Filter.Search.HoldFocus = true; - } - - protected override bool OnExiting(Screen next) - { - Filter.Search.HoldFocus = false; - return base.OnExiting(next); - } - - protected override void OnResuming(Screen last) - { - base.OnResuming(last); - Filter.Search.HoldFocus = true; - } - - protected override void OnSuspending(Screen next) - { - base.OnSuspending(next); - Filter.Search.HoldFocus = false; - } - - private void filterRooms() - { - search.SearchTerm = Filter.Search.Current.Value ?? string.Empty; - - foreach (DrawableRoom r in RoomsContainer.Children) - { - r.MatchingFilter = r.MatchingFilter && - r.Room.Availability.Value == (RoomAvailability)Filter.Tabs.Current.Value; - } - } - - private void didSelect(DrawableRoom room) - { - RoomsContainer.Children.ForEach(c => - { - if (c != room) - c.State = SelectionState.NotSelected; - }); - - Inspector.Room = room.Room; - - // open the room if its selected and is clicked again - if (room.State == SelectionState.Selected) - Push(new Match.Match(room.Room)); - } - - private class RoomsFilterContainer : FillFlowContainer, IHasFilterableChildren - { - public IEnumerable FilterTerms => new string[] { }; - public IEnumerable FilterableChildren => Children; - - public bool MatchingFilter - { - set - { - if (value) - InvalidateLayout(); - } - } - - public RoomsFilterContainer() - { - LayoutDuration = 200; - LayoutEasing = Easing.OutQuint; - } - } - } -} diff --git a/osu.Game/Screens/Multi/Screens/Match/Header.cs b/osu.Game/Screens/Multi/Screens/Match/Header.cs deleted file mode 100644 index c2de50fa65..0000000000 --- a/osu.Game/Screens/Multi/Screens/Match/Header.cs +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -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.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.SearchableList; -using osuTK.Graphics; - -namespace osu.Game.Screens.Multi.Screens.Match -{ - public class Header : Container - { - public const float HEIGHT = 200; - - private readonly Box tabStrip; - private readonly UpdateableBeatmapSetCover cover; - - public readonly PageTabControl Tabs; - - public BeatmapSetInfo BeatmapSet - { - set => cover.BeatmapSet = value; - } - - public Action OnRequestSelectBeatmap; - - public Header() - { - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - - BeatmapSelectButton beatmapButton; - Children = new Drawable[] - { - cover = new UpdateableBeatmapSetCover - { - RelativeSizeAxes = Axes.Both, - Masking = true, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black.Opacity(0.5f)), - }, - tabStrip = new Box - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = 1, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, - Children = new Drawable[] - { - new Container - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = 200, - Padding = new MarginPadding { Vertical = 5 }, - Child = beatmapButton = new BeatmapSelectButton - { - RelativeSizeAxes = Axes.Both, - }, - }, - Tabs = new PageTabControl - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - }, - }, - }, - }; - - beatmapButton.Action = () => OnRequestSelectBeatmap?.Invoke(); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - tabStrip.Colour = colours.Yellow; - } - - private class BeatmapSelectButton : OsuClickableContainer - { - private const float corner_radius = 5; - private const float bg_opacity = 0.5f; - private const float transition_duration = 100; - - private readonly Box bg; - private readonly Container border; - - public BeatmapSelectButton() - { - Masking = true; - CornerRadius = corner_radius; - - Children = new Drawable[] - { - bg = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = bg_opacity, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = @"Exo2.0-Bold", - Text = "Select Beatmap", - }, - border = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius, - BorderThickness = 4, - Alpha = 0, - Child = new Box // needs a child to show the border - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - border.BorderColour = colours.Yellow; - } - - protected override bool OnHover(HoverEvent e) - { - border.FadeIn(transition_duration); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - base.OnHoverLost(e); - border.FadeOut(transition_duration); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - bg.FadeTo(0.75f, 1000, Easing.Out); - return base.OnMouseDown(e); - } - - protected override bool OnMouseUp(MouseUpEvent e) - { - bg.FadeTo(bg_opacity, transition_duration); - return base.OnMouseUp(e); - } - } - } - - public enum MatchHeaderPage - { - Settings, - Room, - } -} diff --git a/osu.Game/Screens/Multi/Screens/Match/Info.cs b/osu.Game/Screens/Multi/Screens/Match/Info.cs deleted file mode 100644 index a2c056c8bd..0000000000 --- a/osu.Game/Screens/Multi/Screens/Match/Info.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Extensions; -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.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.SearchableList; -using osu.Game.Screens.Multi.Components; -using osuTK; - -namespace osu.Game.Screens.Multi.Screens.Match -{ - public class Info : Container - { - public const float HEIGHT = 128; - - private readonly OsuSpriteText name, availabilityStatus; - private readonly BeatmapTypeInfo beatmapTypeInfo; - private readonly ReadyButton readyButton; - - private OsuColour colours; - - public Bindable Ready => readyButton.Ready; - - public string Name - { - set { name.Text = value; } - } - - private RoomAvailability availability; - public RoomAvailability Availability - { - set - { - if (value == availability) return; - availability = value; - - if (IsLoaded) - updateAvailabilityStatus(); - } - } - - private RoomStatus status; - public RoomStatus Status - { - set - { - if (value == status) return; - status = value; - - if (IsLoaded) - updateAvailabilityStatus(); - } - } - - public BeatmapInfo Beatmap - { - set { beatmapTypeInfo.Beatmap = value; } - } - - public GameType Type - { - set { beatmapTypeInfo.Type = value; } - } - - public Info() - { - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"28242d"), - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Padding = new MarginPadding { Vertical = 20 }, - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - name = new OsuSpriteText - { - TextSize = 30, - }, - availabilityStatus = new OsuSpriteText - { - TextSize = 14, - }, - }, - }, - beatmapTypeInfo = new BeatmapTypeInfo - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }, - }, - }, - readyButton = new ReadyButton - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Size = new Vector2(200, 1), - Padding = new MarginPadding { Vertical = 10 }, - }, - }, - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - this.colours = colours; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateAvailabilityStatus(); - } - - private void updateAvailabilityStatus() - { - if (status != null) - { - availabilityStatus.FadeColour(status.GetAppropriateColour(colours), 100); - availabilityStatus.Text = $"{availability.GetDescription()}, {status.Message}"; - } - } - - private class ReadyButton : TriangleButton - { - public readonly Bindable Ready = new Bindable(); - - protected override SpriteText CreateText() => new OsuSpriteText - { - Depth = -1, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Font = @"Exo2.0-Light", - TextSize = 30, - }; - - [BackgroundDependencyLoader] - private void load() - { - BackgroundColour = OsuColour.FromHex(@"1187aa"); - Triangles.ColourLight = OsuColour.FromHex(@"277b9c"); - Triangles.ColourDark = OsuColour.FromHex(@"1f6682"); - Triangles.TriangleScale = 1.5f; - - Container active; - Add(active = new Container - { - RelativeSizeAxes = Axes.Both, - Alpha = 0f, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.15f, - Blending = BlendingMode.Additive, - }, - }); - - Action = () => Ready.Value = !Ready.Value; - - Ready.BindValueChanged(value => - { - if (value) - { - Text = "Not Ready"; - active.FadeIn(200); - } - else - { - Text = "Ready"; - active.FadeOut(200); - } - }, true); - } - } - } -} diff --git a/osu.Game/Screens/Multi/Screens/Match/Match.cs b/osu.Game/Screens/Multi/Screens/Match/Match.cs deleted file mode 100644 index f7d98df60e..0000000000 --- a/osu.Game/Screens/Multi/Screens/Match/Match.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Screens.Match.Settings; -using osu.Game.Screens.Select; -using osu.Game.Users; - -namespace osu.Game.Screens.Multi.Screens.Match -{ - public class Match : MultiplayerScreen - { - private readonly Room room; - private readonly Participants participants; - - private readonly Bindable nameBind = new Bindable(); - private readonly Bindable statusBind = new Bindable(); - private readonly Bindable availabilityBind = new Bindable(); - private readonly Bindable typeBind = new Bindable(); - private readonly Bindable beatmapBind = new Bindable(); - private readonly Bindable maxParticipantsBind = new Bindable(); - private readonly Bindable> participantsBind = new Bindable>(); - - protected override Container TransitionContent => participants; - - public override string Type => "room"; - public override string Title => room.Name.Value; - - public Match(Room room) - { - this.room = room; - Header header; - RoomSettingsOverlay settings; - Info info; - - Children = new Drawable[] - { - header = new Header - { - Depth = -1, - }, - info = new Info - { - Margin = new MarginPadding { Top = Header.HEIGHT }, - }, - participants = new Participants - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = Header.HEIGHT + Info.HEIGHT }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = Header.HEIGHT }, - Child = settings = new RoomSettingsOverlay(room) - { - RelativeSizeAxes = Axes.Both, - Height = 0.9f, - }, - }, - }; - - header.OnRequestSelectBeatmap = () => Push(new MatchSongSelect()); - - beatmapBind.BindTo(room.Beatmap); - beatmapBind.BindValueChanged(b => - { - header.BeatmapSet = b?.BeatmapSet; - info.Beatmap = b; - }, true); - - header.Tabs.Current.ValueChanged += t => - { - if (t == MatchHeaderPage.Settings) - settings.Show(); - else - settings.Hide(); - }; - - settings.StateChanged += s => - { - if (s == Visibility.Hidden) - header.Tabs.Current.Value = MatchHeaderPage.Room; - }; - - nameBind.BindTo(room.Name); - nameBind.BindValueChanged(n => info.Name = n, true); - - statusBind.BindTo(room.Status); - statusBind.BindValueChanged(s => info.Status = s, true); - - availabilityBind.BindTo(room.Availability); - availabilityBind.BindValueChanged(a => info.Availability = a, true); - - typeBind.BindTo(room.Type); - typeBind.BindValueChanged(t => info.Type = t, true); - - maxParticipantsBind.BindTo(room.MaxParticipants); - maxParticipantsBind.BindValueChanged(m => { participants.Max = m; }, true); - - participantsBind.BindTo(room.Participants); - participantsBind.BindValueChanged(p => participants.Users = p, true); - } - } -} diff --git a/osu.Game/Screens/Multi/Screens/Match/Settings/RoomSettingsOverlay.cs b/osu.Game/Screens/Multi/Screens/Match/Settings/RoomSettingsOverlay.cs deleted file mode 100644 index d45ba48f0e..0000000000 --- a/osu.Game/Screens/Multi/Screens/Match/Settings/RoomSettingsOverlay.cs +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Configuration; -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.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.SearchableList; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Multi.Screens.Match.Settings -{ - public class RoomSettingsOverlay : FocusedOverlayContainer - { - private const float transition_duration = 350; - private const float field_padding = 45; - - private readonly Bindable nameBind = new Bindable(); - private readonly Bindable availabilityBind = new Bindable(); - private readonly Bindable typeBind = new Bindable(); - private readonly Bindable maxParticipantsBind = new Bindable(); - - private readonly Container content; - private readonly OsuSpriteText typeLabel; - - protected readonly OsuTextBox NameField, MaxParticipantsField; - protected readonly RoomAvailabilityPicker AvailabilityPicker; - protected readonly GameTypePicker TypePicker; - protected readonly TriangleButton ApplyButton; - - public RoomSettingsOverlay(Room room) - { - Masking = true; - - Child = content = new Container - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Y, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"28242d"), - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 35, Bottom = 75, Horizontal = SearchableListOverlay.WIDTH_PADDING }, - Children = new[] - { - new SectionContainer - { - Padding = new MarginPadding { Right = field_padding / 2 }, - Children = new[] - { - new Section("ROOM NAME") - { - Child = NameField = new SettingsTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - OnCommit = (sender, text) => apply(), - }, - }, - new Section("ROOM VISIBILITY") - { - Child = AvailabilityPicker = new RoomAvailabilityPicker(), - }, - new Section("GAME TYPE") - { - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(7), - Children = new Drawable[] - { - TypePicker = new GameTypePicker - { - RelativeSizeAxes = Axes.X, - }, - typeLabel = new OsuSpriteText - { - TextSize = 14, - }, - }, - }, - }, - }, - }, - new SectionContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Padding = new MarginPadding { Left = field_padding / 2 }, - Children = new[] - { - new Section("MAX PARTICIPANTS") - { - Child = MaxParticipantsField = new SettingsNumberTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - OnCommit = (sender, text) => apply(), - }, - }, - new Section("PASSWORD (OPTIONAL)") - { - Child = new SettingsPasswordTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - OnCommit = (sender, text) => apply(), - }, - }, - }, - }, - }, - }, - ApplyButton = new ApplySettingsButton - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Size = new Vector2(230, 35), - Margin = new MarginPadding { Bottom = 20 }, - Action = apply, - }, - }, - }; - - TypePicker.Current.ValueChanged += t => typeLabel.Text = t.Name; - - nameBind.ValueChanged += n => NameField.Text = n; - availabilityBind.ValueChanged += a => AvailabilityPicker.Current.Value = a; - typeBind.ValueChanged += t => TypePicker.Current.Value = t; - maxParticipantsBind.ValueChanged += m => MaxParticipantsField.Text = m?.ToString(); - - nameBind.BindTo(room.Name); - availabilityBind.BindTo(room.Availability); - typeBind.BindTo(room.Type); - maxParticipantsBind.BindTo(room.MaxParticipants); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - typeLabel.Colour = colours.Yellow; - } - - protected override void PopIn() - { - // reapply the rooms values if the overlay was completely closed - if (content.Y == -1) - { - nameBind.TriggerChange(); - availabilityBind.TriggerChange(); - typeBind.TriggerChange(); - maxParticipantsBind.TriggerChange(); - } - - content.MoveToY(0, transition_duration, Easing.OutQuint); - } - - protected override void PopOut() - { - content.MoveToY(-1, transition_duration, Easing.InSine); - } - - private void apply() - { - nameBind.Value = NameField.Text; - availabilityBind.Value = AvailabilityPicker.Current.Value; - typeBind.Value = TypePicker.Current.Value; - - if (int.TryParse(MaxParticipantsField.Text, out int max)) - maxParticipantsBind.Value = max; - else - maxParticipantsBind.Value = null; - - Hide(); - } - - private class SettingsTextBox : OsuTextBox - { - protected override Color4 BackgroundUnfocused => Color4.Black; - protected override Color4 BackgroundFocused => Color4.Black; - } - - private class SettingsNumberTextBox : SettingsTextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } - - private class SettingsPasswordTextBox : OsuPasswordTextBox - { - protected override Color4 BackgroundUnfocused => Color4.Black; - protected override Color4 BackgroundFocused => Color4.Black; - } - - private class SectionContainer : FillFlowContainer
- { - public SectionContainer() - { - RelativeSizeAxes = Axes.Both; - Width = 0.5f; - Direction = FillDirection.Vertical; - Spacing = new Vector2(field_padding); - } - } - - private class Section : Container - { - private readonly Container content; - - protected override Container Content => content; - - public Section(string title) - { - AutoSizeAxes = Axes.Y; - RelativeSizeAxes = Axes.X; - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new OsuSpriteText - { - TextSize = 12, - Font = @"Exo2.0-Bold", - Text = title.ToUpper(), - }, - content = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }, - }, - }; - } - } - - private class ApplySettingsButton : TriangleButton - { - public ApplySettingsButton() - { - Text = "Apply"; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = colours.Yellow; - Triangles.ColourLight = colours.YellowLight; - Triangles.ColourDark = colours.YellowDark; - } - } - } -} diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index c6f39b4705..8e9b0a0a5c 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Play.HUD { private const int fade_duration = 1000; + public bool DisplayUnrankedText = true; + private readonly Bindable> current = new Bindable>(); public Bindable> Current @@ -41,6 +43,8 @@ namespace osu.Game.Screens.Play.HUD public ModDisplay() { + AutoSizeAxes = Axes.Both; + Children = new Drawable[] { iconsContainer = new ReverseChildIDFillFlowContainer @@ -53,7 +57,6 @@ namespace osu.Game.Screens.Play.HUD }, unrankedText = new OsuSpriteText { - AlwaysPresent = true, Anchor = Anchor.BottomCentre, Origin = Anchor.TopCentre, Text = @"/ UNRANKED /", @@ -89,7 +92,7 @@ namespace osu.Game.Screens.Play.HUD private void appearTransform() { - if (Current.Value.Any(m => !m.Ranked)) + if (DisplayUnrankedText && Current.Value.Any(m => !m.Ranked)) unrankedText.FadeInFromZero(fade_duration, Easing.OutQuint); else unrankedText.Hide(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1429675ddd..14dc644100 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -28,6 +28,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Storyboards.Drawables; @@ -284,10 +285,10 @@ namespace osu.Game.Screens.Play if (!IsCurrentScreen) return; var score = CreateScore(); - if (RulesetContainer.Replay == null) + if (RulesetContainer.ReplayScore == null) scoreManager.Import(score, true); - Push(new SoloResults(score)); + Push(CreateResults(score)); onCompletionEvent = null; }); @@ -296,11 +297,12 @@ namespace osu.Game.Screens.Play protected virtual ScoreInfo CreateScore() { - var score = new ScoreInfo + var score = RulesetContainer.ReplayScore?.ScoreInfo ?? new ScoreInfo { Beatmap = Beatmap.Value.BeatmapInfo, Ruleset = ruleset, - User = api.LocalUser.Value + Mods = Beatmap.Value.Mods.Value.ToArray(), + User = api.LocalUser.Value, }; ScoreProcessor.PopulateScore(score); @@ -431,5 +433,7 @@ namespace osu.Game.Screens.Play if (storyboardVisible && beatmap.Storyboard.ReplacesBackground) Background?.FadeTo(0, BACKGROUND_FADE_DURATION, Easing.OutQuint); } + + protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score); } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index bf44a60473..ded25f013a 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -24,6 +25,7 @@ namespace osu.Game.Screens.Play { public class PlayerLoader : ScreenWithBeatmapBackground { + private readonly Func createPlayer; private static readonly Vector2 background_blur = new Vector2(15); private Player player; @@ -35,15 +37,15 @@ namespace osu.Game.Screens.Play private Task loadTask; - public PlayerLoader(Player player) + public PlayerLoader(Func createPlayer) { - this.player = player; + this.createPlayer = createPlayer; + } - player.RestartRequested = () => - { - hideOverlays = true; - ValidForResume = true; - }; + private void restartRequested() + { + hideOverlays = true; + ValidForResume = true; } [BackgroundDependencyLoader] @@ -71,7 +73,7 @@ namespace osu.Game.Screens.Play } }); - loadTask = LoadComponentAsync(player, playerLoaded); + loadNewPlayer(); } private void playerLoaded(Player player) => info.Loading = false; @@ -85,15 +87,22 @@ namespace osu.Game.Screens.Play info.Loading = true; //we will only be resumed if the player has requested a re-run (see ValidForResume setting above) - loadTask = LoadComponentAsync(player = new Player - { - RestartCount = player.RestartCount + 1, - RestartRequested = player.RestartRequested, - }, playerLoaded); + loadNewPlayer(); this.Delay(400).Schedule(pushWhenLoaded); } + private void loadNewPlayer() + { + var restartCount = player?.RestartCount + 1 ?? 0; + + player = createPlayer(); + player.RestartCount = restartCount; + player.RestartRequested = restartRequested; + + loadTask = LoadComponentAsync(player, playerLoaded); + } + private void contentIn() { Content.ScaleTo(1, 650, Easing.OutQuint); diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index fe77fd57f2..04cf922d74 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); - RulesetContainer.SetReplay(score.Replay); + RulesetContainer.SetReplayScore(score); } protected override ScoreInfo CreateScore() => score.ScoreInfo; diff --git a/osu.Game/Screens/Play/SoloResults.cs b/osu.Game/Screens/Play/SoloResults.cs index 5e318e95d1..c06a5c3f68 100644 --- a/osu.Game/Screens/Play/SoloResults.cs +++ b/osu.Game/Screens/Play/SoloResults.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play protected override IEnumerable CreateResultPages() => new IResultPageInfo[] { new ScoreOverviewPageInfo(Score, Beatmap), - new BeatmapLeaderboardPageInfo(Score, Beatmap) + new LocalLeaderboardPageInfo(Score, Beatmap) }; } } diff --git a/osu.Game/Screens/Ranking/Pages/RankingResultsPage.cs b/osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs similarity index 85% rename from osu.Game/Screens/Ranking/Pages/RankingResultsPage.cs rename to osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs index 4c98e476c4..8ab45f2f1f 100644 --- a/osu.Game/Screens/Ranking/Pages/RankingResultsPage.cs +++ b/osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs @@ -12,9 +12,9 @@ using osuTK; namespace osu.Game.Screens.Ranking.Pages { - public class RankingResultsPage : ResultsPage + public class LocalLeaderboardPage : ResultsPage { - public RankingResultsPage(ScoreInfo score, WorkingBeatmap beatmap = null) + public LocalLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap = null) : base(score, beatmap) { } @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Ranking.Pages { new Box { - Colour = colours.GrayE, + Colour = colours.Gray6, RelativeSizeAxes = Axes.Both, }, new BeatmapLeaderboard diff --git a/osu.Game/Screens/Ranking/Types/BeatmapLeaderboardPageInfo.cs b/osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs similarity index 59% rename from osu.Game/Screens/Ranking/Types/BeatmapLeaderboardPageInfo.cs rename to osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs index 2b192d4bcd..20eb75ff6f 100644 --- a/osu.Game/Screens/Ranking/Types/BeatmapLeaderboardPageInfo.cs +++ b/osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs @@ -8,21 +8,21 @@ using osu.Game.Screens.Ranking.Pages; namespace osu.Game.Screens.Ranking.Types { - public class BeatmapLeaderboardPageInfo : IResultPageInfo + public class LocalLeaderboardPageInfo : IResultPageInfo { private readonly ScoreInfo score; private readonly WorkingBeatmap beatmap; - public BeatmapLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap) + public LocalLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap) { this.score = score; this.beatmap = beatmap; } - public FontAwesome Icon => FontAwesome.fa_list; + public FontAwesome Icon => FontAwesome.fa_user; - public string Name => @"Beatmap Leaderboard"; + public string Name => @"Local Leaderboard"; - public ResultsPage CreatePage() => new RankingResultsPage(score, beatmap); + public ResultsPage CreatePage() => new LocalLeaderboardPage(score, beatmap); } } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 339392d5cf..5763b84e89 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -1,13 +1,36 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using Humanizer; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi; + namespace osu.Game.Screens.Select { - public class MatchSongSelect : SongSelect + public class MatchSongSelect : SongSelect, IMultiplayerSubScreen { + public Action Selected; + + public string ShortTitle => "song selection"; + public override string Title => ShortTitle.Humanize(); + protected override bool OnStart() { - if (IsCurrentScreen) Exit(); + var item = new PlaylistItem + { + Beatmap = Beatmap.Value.BeatmapInfo, + Ruleset = Ruleset.Value, + RulesetID = Ruleset.Value.ID ?? 0 + }; + + item.RequiredMods.AddRange(SelectedMods.Value); + + Selected?.Invoke(item); + + if (IsCurrentScreen) + Exit(); + return true; } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index cbe22d968a..a7de93b11d 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Select SampleConfirm?.Play(); - LoadComponentAsync(player = new PlayerLoader(new Player()), l => + LoadComponentAsync(player = new PlayerLoader(() => new Player()), l => { if (IsCurrentScreen) Push(player); }); diff --git a/osu.Game/Tests/Visual/OsuTestCase.cs b/osu.Game/Tests/Visual/OsuTestCase.cs index 67a13bd850..80c9b2ab47 100644 --- a/osu.Game/Tests/Visual/OsuTestCase.cs +++ b/osu.Game/Tests/Visual/OsuTestCase.cs @@ -29,6 +29,9 @@ namespace osu.Game.Tests.Visual { Dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + // This is the earliest we can get OsuGameBase, which is used by the dummy working beatmap to find textures + beatmap.Default = new DummyWorkingBeatmap(Dependencies.Get()); + Dependencies.CacheAs(beatmap); Dependencies.CacheAs(beatmap); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 18addaefb6..103c7c20d6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - + diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index aac799f961..d9f74aade1 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -674,6 +674,127 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste True True True + True + True + o!f – Object Initializer: Anchor&Origin + True + constant("Centre") + 0 + True + True + 2.0 + InCSharpFile + ofao + True + Anchor = Anchor.$anchor$, +Origin = Anchor.$anchor$, + True + True + o!f – InternalChildren = [] + True + True + 2.0 + InCSharpFile + ofic + True + InternalChildren = new Drawable[] +{ + $END$ +}; + True + True + o!f – new GridContainer { .. } + True + True + 2.0 + InCSharpFile + ofgc + True + new GridContainer +{ + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { $END$ }, + new Drawable[] { } + } +}; + True + True + o!f – new FillFlowContainer { .. } + True + True + 2.0 + InCSharpFile + offf + True + new FillFlowContainer +{ + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – new Container { .. } + True + True + 2.0 + InCSharpFile + ofcont + True + new Container +{ + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + $END$ + } +}, + True + True + o!f – BackgroundDependencyLoader load() + True + True + 2.0 + InCSharpFile + ofbdl + True + [BackgroundDependencyLoader] +private void load() +{ + $END$ +} + True + True + o!f – new Box { .. } + True + True + 2.0 + InCSharpFile + ofbox + True + new Box +{ + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, +}, + True + True + o!f – Children = [] + True + True + 2.0 + InCSharpFile + ofc + True + Children = new Drawable[] +{ + $END$ +}; True True True