1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 21:43:04 +08:00

Merge branch 'master' into perform-from-subscreen-support

This commit is contained in:
Dan Balasescu 2021-02-22 11:55:51 +09:00 committed by GitHub
commit 8037b101cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 876 additions and 451 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.215.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.220.0" />
</ItemGroup>
</Project>

View File

@ -17,7 +17,7 @@ using osu.Game.Database;
namespace osu.Android
{
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)]
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]

View File

@ -105,7 +105,7 @@ namespace osu.Desktop
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty;
else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty);
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
// update ruleset
presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom";

View File

@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Tests.Beatmaps;
@ -17,6 +18,10 @@ namespace osu.Game.Rulesets.Catch.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(5.0565038923984691d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new CatchRuleset();

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
@ -17,6 +18,10 @@ namespace osu.Game.Rulesets.Mania.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(2.7646128945056723d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new ManiaRuleset();

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
@ -19,6 +20,11 @@ namespace osu.Game.Rulesets.Osu.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(8.6228371119393064d, "diffcalc-test")]
[TestCase(1.2864585434597433d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new OsuRuleset();

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Taiko.Difficulty;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests
@ -18,6 +19,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(3.1473940254109078d, "diffcalc-test")]
[TestCase(3.1473940254109078d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new TaikoRuleset();

View File

@ -118,9 +118,9 @@ namespace osu.Game.Tests.Rulesets
public BindableNumber<double> Frequency => throw new NotImplementedException();
public BindableNumber<double> Tempo => throw new NotImplementedException();
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException();

View File

@ -56,9 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay
pauseAndConfirm();
resume();
confirmClockRunning(false);
confirmPauseOverlayShown(false);
confirmPausedWithNoOverlay();
AddStep("click to resume", () => InputManager.Click(MouseButton.Left));
confirmClockRunning(true);
@ -71,15 +69,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1);
pauseAndConfirm();
resume();
confirmClockRunning(false);
confirmPauseOverlayShown(false);
confirmPausedWithNoOverlay();
pauseAndConfirm();
AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
confirmPaused();
confirmNotExited();
}
[Test]
@ -94,33 +91,54 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
public void TestPauseTooSoon()
public void TestUserPauseWhenPauseNotAllowed()
{
AddStep("disable pause support", () => Player.Configuration.AllowPause = false);
pauseFromUserExitKey();
confirmExited();
}
[Test]
public void TestUserPauseDuringCooldownTooSoon()
{
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm();
resume();
pause();
pauseFromUserExitKey();
confirmClockRunning(true);
confirmPauseOverlayShown(false);
confirmResumed();
confirmNotExited();
}
[Test]
public void TestExitTooSoon()
public void TestQuickExitDuringCooldownTooSoon()
{
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm();
resume();
AddStep("pause via exit key", () => Player.ExitViaQuickExit());
confirmResumed();
AddAssert("exited", () => !Player.IsCurrentScreen());
}
[Test]
public void TestExitSoonAfterResumeSucceeds()
{
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
pauseAndConfirm();
resume();
AddStep("exit too soon", () => Player.Exit());
AddStep("exit quick", () => Player.Exit());
confirmClockRunning(true);
confirmPauseOverlayShown(false);
AddAssert("not exited", () => Player.IsCurrentScreen());
confirmResumed();
AddAssert("exited", () => !Player.IsCurrentScreen());
}
[Test]
@ -131,22 +149,37 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmClockRunning(false);
pause();
confirmClockRunning(false);
confirmPauseOverlayShown(false);
AddStep("pause via forced pause", () => Player.Pause());
confirmPausedWithNoOverlay();
AddAssert("fail overlay still shown", () => Player.FailOverlayVisible);
exitAndConfirm();
}
[Test]
public void TestExitFromFailedGameplay()
public void TestExitFromFailedGameplayAfterFailAnimation()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("exit", () => Player.Exit());
AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible);
confirmClockRunning(false);
AddStep("exit via user pause", () => Player.ExitViaPause());
confirmExited();
}
[Test]
public void TestExitFromFailedGameplayDuringFailAnimation()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
// will finish the fail animation and show the fail/pause screen.
AddStep("attempt exit via pause key", () => Player.ExitViaPause());
AddAssert("fail overlay shown", () => Player.FailOverlayVisible);
// will actually exit.
AddStep("exit via pause key", () => Player.ExitViaPause());
confirmExited();
}
@ -245,7 +278,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void pauseAndConfirm()
{
pause();
pauseFromUserExitKey();
confirmPaused();
}
@ -257,7 +290,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void exitAndConfirm()
{
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
confirmNotExited();
AddStep("exit", () => Player.Exit());
confirmExited();
confirmNoTrackAdjustments();
@ -266,7 +299,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void confirmPaused()
{
confirmClockRunning(false);
AddAssert("player not exited", () => Player.IsCurrentScreen());
confirmNotExited();
AddAssert("player not failed", () => !Player.HasFailed);
AddAssert("pause overlay shown", () => Player.PauseOverlayVisible);
}
@ -277,18 +310,22 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmPauseOverlayShown(false);
}
private void confirmExited()
private void confirmPausedWithNoOverlay()
{
AddUntilStep("player exited", () => !Player.IsCurrentScreen());
confirmClockRunning(false);
confirmPauseOverlayShown(false);
}
private void confirmExited() => AddUntilStep("player exited", () => !Player.IsCurrentScreen());
private void confirmNotExited() => AddAssert("player not exited", () => Player.IsCurrentScreen());
private void confirmNoTrackAdjustments()
{
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
}
private void restart() => AddStep("restart", () => Player.Restart());
private void pause() => AddStep("pause", () => Player.Pause());
private void pauseFromUserExitKey() => AddStep("user pause", () => Player.ExitViaPause());
private void resume() => AddStep("resume", () => Player.Resume());
private void confirmPauseOverlayShown(bool isShown) =>
@ -307,6 +344,10 @@ namespace osu.Game.Tests.Visual.Gameplay
public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible;
public void ExitViaPause() => PerformExit(true);
public void ExitViaQuickExit() => PerformExit(false);
public override void OnEntering(IScreen last)
{
base.OnEntering(last);

View File

@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
@ -50,6 +51,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps]
public override void SetUpSteps()
{
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = lookupCache.GetUserAsync(1).Result);
AddStep("create leaderboard", () =>
{
leaderboard?.Expire();
@ -85,6 +88,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestScoreUpdates()
{
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
}
[Test]

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
@ -155,7 +156,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Id = i,
Username = $"User {i}",
CurrentModeRank = RNG.Next(1, 100000),
RulesetsStatistics = new Dictionary<string, UserStatistics>
{
{
Ruleset.Value.ShortName,
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
}
},
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
@ -193,7 +200,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Id = 0,
Username = "User 0",
CurrentModeRank = RNG.Next(1, 100000),
RulesetsStatistics = new Dictionary<string, UserStatistics>
{
{
Ruleset.Value.ShortName,
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
}
},
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});

View File

@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
@ -115,6 +116,8 @@ namespace osu.Game.Tests.Visual.Navigation
public new Bindable<RulesetInfo> Ruleset => base.Ruleset;
public new Bindable<IReadOnlyList<Mod>> SelectedMods => base.SelectedMods;
// if we don't do this, when running under nUnit the version that gets populated is that of nUnit.
public override string Version => "test game";

View File

@ -11,7 +11,9 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO;
@ -41,6 +43,30 @@ namespace osu.Game.Tests.Visual.Navigation
exitViaEscapeAndConfirm();
}
[Test]
public void TestRetryFromResults()
{
Player player = null;
ResultsScreen results = null;
WorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new TestSongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("set autoplay", () => Game.SelectedMods.Value = new[] { new OsuModAutoplay() });
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
AddStep("seek to end", () => beatmap().Track.Seek(beatmap().Track.Length));
AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded);
AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player);
}
[TestCase(true)]
[TestCase(false)]
public void TestSongContinuesAfterExitPlayer(bool withUserPause)

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online
{
graph.Statistics.Value = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 123456 },
GlobalRank = 123456,
PP = 12345,
};
});
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online
{
graph.Statistics.Value = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 89000 },
GlobalRank = 89000,
PP = 12345,
RankHistory = new User.RankHistoryData
{
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Online
{
graph.Statistics.Value = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 89000 },
GlobalRank = 89000,
PP = 12345,
RankHistory = new User.RankHistoryData
{
@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Online
{
graph.Statistics.Value = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 12000 },
GlobalRank = 12000,
PP = 12345,
RankHistory = new User.RankHistoryData
{
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Online
{
graph.Statistics.Value = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 12000 },
GlobalRank = 12000,
PP = 12345,
RankHistory = new User.RankHistoryData
{

View File

@ -33,7 +33,8 @@ namespace osu.Game.Tests.Visual.Online
ProfileOrder = new[] { "me" },
Statistics = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 2148, Country = 1 },
GlobalRank = 2148,
CountryRank = 1,
PP = 4567.89m,
Level = new UserStatistics.LevelInfo
{

View File

@ -113,11 +113,11 @@ namespace osu.Game.Tournament.Tests
},
Players =
{
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } },
new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 12 } },
new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 16 } },
new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 20 } },
new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 24 } },
new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 30 } },
}
}
},

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Models
{
get
{
var ranks = Players.Select(p => p.Statistics?.Ranks.Global)
var ranks = Players.Select(p => p.Statistics?.GlobalRank)
.Where(i => i.HasValue)
.Select(i => i.Value)
.ToArray();

View File

@ -250,7 +250,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
};
foreach (var p in team.Players)
fill.Add(new RowDisplay(p.Username, p.Statistics?.Ranks.Global?.ToString("\\##,0") ?? "-"));
fill.Add(new RowDisplay(p.Username, p.Statistics?.GlobalRank?.ToString("\\##,0") ?? "-"));
}
internal class RowDisplay : CompositeDrawable

View File

@ -150,9 +150,9 @@ namespace osu.Game.Tournament
{
foreach (var p in t.Players)
{
if (string.IsNullOrEmpty(p.Username) || p.Statistics == null)
if (string.IsNullOrEmpty(p.Username) || p.Statistics?.GlobalRank == null)
{
PopulateUser(p);
PopulateUser(p, immediate: true);
addedInfo = true;
}
}
@ -211,12 +211,14 @@ namespace osu.Game.Tournament
return addedInfo;
}
public void PopulateUser(User user, Action success = null, Action failure = null)
public void PopulateUser(User user, Action success = null, Action failure = null, bool immediate = false)
{
var req = new GetUserRequest(user.Id, Ruleset.Value);
req.Success += res =>
{
user.Id = res.Id;
user.Username = res.Username;
user.Statistics = res.Statistics;
user.Country = res.Country;
@ -231,7 +233,10 @@ namespace osu.Game.Tournament
failure?.Invoke();
};
API.Queue(req);
if (immediate)
API.Perform(req);
else
API.Queue(req);
}
protected override void LoadComplete()

View File

@ -29,6 +29,14 @@ namespace osu.Game.Collections
/// </summary>
protected virtual bool ShowManageCollectionsItem => true;
private readonly BindableWithCurrent<CollectionFilterMenuItem> current = new BindableWithCurrent<CollectionFilterMenuItem>();
public new Bindable<CollectionFilterMenuItem> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly IBindableList<BeatmapCollection> collections = new BindableList<BeatmapCollection>();
private readonly IBindableList<BeatmapInfo> beatmaps = new BindableList<BeatmapInfo>();
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
@ -36,25 +44,28 @@ namespace osu.Game.Collections
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
public CollectionFilterDropdown()
{
ItemSource = filters;
}
[BackgroundDependencyLoader(permitNulls: true)]
private void load([CanBeNull] CollectionManager collectionManager)
{
if (collectionManager != null)
collections.BindTo(collectionManager.Collections);
collections.CollectionChanged += (_, __) => collectionsChanged();
collectionsChanged();
Current.Value = new AllBeatmapsCollectionFilterMenuItem();
}
protected override void LoadComplete()
{
base.LoadComplete();
if (collectionManager != null)
collections.BindTo(collectionManager.Collections);
// Dropdown has logic which triggers a change on the bindable with every change to the contained items.
// This is not desirable here, as it leads to multiple filter operations running even though nothing has changed.
// An extra bindable is enough to subvert this behaviour.
base.Current = Current;
collections.BindCollectionChanged((_, __) => collectionsChanged(), true);
Current.BindValueChanged(filterChanged, true);
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using osu.Framework.Bindables;
@ -9,7 +10,7 @@ namespace osu.Game.Collections
/// <summary>
/// A <see cref="BeatmapCollection"/> filter.
/// </summary>
public class CollectionFilterMenuItem
public class CollectionFilterMenuItem : IEquatable<CollectionFilterMenuItem>
{
/// <summary>
/// The collection to filter beatmaps from.
@ -33,6 +34,11 @@ namespace osu.Game.Collections
Collection = collection;
CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable<string>("All beatmaps");
}
public bool Equals(CollectionFilterMenuItem other)
=> other != null && CollectionName.Value == other.CollectionName.Value;
public override int GetHashCode() => CollectionName.Value.GetHashCode();
}
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem

View File

@ -139,35 +139,43 @@ namespace osu.Game.Collections
PostNotification?.Invoke(notification);
var collection = readCollections(stream, notification);
bool importCompleted = false;
Schedule(() =>
{
importCollections(collection);
importCompleted = true;
});
while (!IsDisposed && !importCompleted)
await Task.Delay(10);
await importCollections(collection);
notification.CompletionText = $"Imported {collection.Count} collections";
notification.State = ProgressNotificationState.Completed;
}
private void importCollections(List<BeatmapCollection> newCollections)
private Task importCollections(List<BeatmapCollection> newCollections)
{
foreach (var newCol in newCollections)
{
var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name);
if (existing == null)
Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
var tcs = new TaskCompletionSource<bool>();
foreach (var newBeatmap in newCol.Beatmaps)
Schedule(() =>
{
try
{
if (!existing.Beatmaps.Contains(newBeatmap))
existing.Beatmaps.Add(newBeatmap);
foreach (var newCol in newCollections)
{
var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name);
if (existing == null)
Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
foreach (var newBeatmap in newCol.Beatmaps)
{
if (!existing.Beatmaps.Contains(newBeatmap))
existing.Beatmaps.Add(newBeatmap);
}
}
tcs.SetResult(true);
}
}
catch (Exception e)
{
Logger.Error(e, "Failed to import collection.");
tcs.SetException(e);
}
});
return tcs.Task;
}
private List<BeatmapCollection> readCollections(Stream stream, ProgressNotification notification = null)

View File

@ -16,12 +16,12 @@ namespace osu.Game.Configuration
[Description("Hit Error (right)")]
HitErrorRight,
[Description("Hit Error (bottom)")]
HitErrorBottom,
[Description("Hit Error (left+right)")]
HitErrorBoth,
[Description("Hit Error (bottom)")]
HitErrorBottom,
[Description("Colour (left)")]
ColourLeft,

View File

@ -36,18 +36,23 @@ namespace osu.Game.Online.Multiplayer
[Key(5)]
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();
[Key(6)]
public long PlaylistItemId { get; set; }
public bool Equals(MultiplayerRoomSettings other)
=> BeatmapID == other.BeatmapID
&& BeatmapChecksum == other.BeatmapChecksum
&& RequiredMods.SequenceEqual(other.RequiredMods)
&& AllowedMods.SequenceEqual(other.AllowedMods)
&& RulesetID == other.RulesetID
&& Name.Equals(other.Name, StringComparison.Ordinal);
&& Name.Equals(other.Name, StringComparison.Ordinal)
&& PlaylistItemId == other.PlaylistItemId;
public override string ToString() => $"Name:{Name}"
+ $" Beatmap:{BeatmapID} ({BeatmapChecksum})"
+ $" RequiredMods:{string.Join(',', RequiredMods)}"
+ $" AllowedMods:{string.Join(',', AllowedMods)}"
+ $" Ruleset:{RulesetID}";
+ $" Ruleset:{RulesetID}"
+ $" Item:{PlaylistItemId}";
}
}

View File

@ -66,6 +66,8 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
public readonly BindableList<int> CurrentMatchPlayingUserIds = new BindableList<int>();
public readonly Bindable<PlaylistItem?> CurrentMatchPlayingItem = new Bindable<PlaylistItem?>();
/// <summary>
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
/// </summary>
@ -92,10 +94,11 @@ namespace osu.Game.Online.Multiplayer
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private Room? apiRoom;
// Only exists for compatibility with old osu-server-spectator build.
// Todo: Can be removed on 2021/02/26.
private long defaultPlaylistItemId;
// Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise.
private long playlistItemId;
private Room? apiRoom;
[BackgroundDependencyLoader]
private void load()
@ -142,7 +145,7 @@ namespace osu.Game.Online.Multiplayer
{
Room = joinedRoom;
apiRoom = room;
playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0;
defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0;
}, cancellationSource.Token);
// Update room settings.
@ -218,7 +221,7 @@ namespace osu.Game.Online.Multiplayer
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods,
AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods
AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods,
});
}
@ -506,14 +509,13 @@ namespace osu.Game.Online.Multiplayer
Room.Settings = settings;
apiRoom.Name.Value = Room.Settings.Name;
// The playlist update is delayed until an online beatmap lookup (below) succeeds.
// In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here.
apiRoom.Playlist.Clear();
// The current item update is delayed until an online beatmap lookup (below) succeeds.
// In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here.
CurrentMatchPlayingItem.Value = null;
RoomUpdated?.Invoke();
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
req.Success += res =>
{
if (cancellationToken.IsCancellationRequested)
@ -540,18 +542,30 @@ namespace osu.Game.Online.Multiplayer
var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
PlaylistItem playlistItem = new PlaylistItem
// Try to retrieve the existing playlist item from the API room.
var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId);
if (playlistItem != null)
updateItem(playlistItem);
else
{
ID = playlistItemId,
Beatmap = { Value = beatmap },
Ruleset = { Value = ruleset.RulesetInfo },
};
// An existing playlist item does not exist, so append a new one.
updateItem(playlistItem = new PlaylistItem());
apiRoom.Playlist.Add(playlistItem);
}
playlistItem.RequiredMods.AddRange(mods);
playlistItem.AllowedMods.AddRange(allowedMods);
CurrentMatchPlayingItem.Value = playlistItem;
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity.
apiRoom.Playlist.Add(playlistItem);
void updateItem(PlaylistItem item)
{
item.ID = settings.PlaylistItemId == 0 ? defaultPlaylistItemId : settings.PlaylistItemId;
item.Beatmap.Value = beatmap;
item.Ruleset.Value = ruleset.RulesetInfo;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(mods);
item.AllowedMods.Clear();
item.AllowedMods.AddRange(allowedMods);
}
}
/// <summary>

View File

@ -15,7 +15,7 @@ namespace osu.Game.Online
/// A <see cref="Container"/> for displaying online content which require a local user to be logged in.
/// Shows its children only when the local user is logged in and supports displaying a placeholder if not.
/// </summary>
public abstract class OnlineViewContainer : Container
public class OnlineViewContainer : Container
{
protected LoadingSpinner LoadingSpinner { get; private set; }
@ -30,7 +30,7 @@ namespace osu.Game.Online
[Resolved]
protected IAPIProvider API { get; private set; }
protected OnlineViewContainer(string placeholderMessage)
public OnlineViewContainer(string placeholderMessage)
{
this.placeholderMessage = placeholderMessage;
}

View File

@ -23,6 +23,12 @@ namespace osu.Game.Online.Rooms
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
/// <summary>
/// Whether this <see cref="PlaylistItem"/> is still a valid selection for the <see cref="Room"/>.
/// </summary>
[JsonProperty("expired")]
public bool Expired { get; set; }
[JsonIgnore]
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();

View File

@ -153,6 +153,12 @@ namespace osu.Game.Online.Rooms
if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value)
Status.Value = new RoomStatusEnded();
// Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended,
// and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room.
// More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room.
if (!(Status.Value is RoomStatusEnded))
other.Playlist.RemoveAll(i => i.Expired);
if (!Playlist.SequenceEqual(other.Playlist))
{
Playlist.Clear();

View File

@ -30,7 +30,7 @@ namespace osu.Game.Overlays
protected List<APIUpdateStream> Streams;
public ChangelogOverlay()
: base(OverlayColourScheme.Purple)
: base(OverlayColourScheme.Purple, false)
{
}

View File

@ -24,6 +24,7 @@ using osu.Game.Overlays.Chat.Tabs;
using osuTK.Input;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Online;
namespace osu.Game.Overlays
{
@ -118,40 +119,47 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both,
},
currentChannelContainer = new Container<DrawableChannel>
new OnlineViewContainer("Sign in to chat")
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Bottom = textbox_height
},
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = textbox_height,
Padding = new MarginPadding
{
Top = padding * 2,
Bottom = padding * 2,
Left = ChatLine.LEFT_PADDING + padding * 2,
Right = padding * 2,
},
Children = new Drawable[]
{
textbox = new FocusedTextBox
currentChannelContainer = new Container<DrawableChannel>
{
RelativeSizeAxes = Axes.Both,
Height = 1,
PlaceholderText = "type your message",
ReleaseFocusOnCommit = false,
HoldFocus = true,
}
}
},
loading = new LoadingSpinner(),
Padding = new MarginPadding
{
Bottom = textbox_height
},
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = textbox_height,
Padding = new MarginPadding
{
Top = padding * 2,
Bottom = padding * 2,
Left = ChatLine.LEFT_PADDING + padding * 2,
Right = padding * 2,
},
Children = new Drawable[]
{
textbox = new FocusedTextBox
{
RelativeSizeAxes = Axes.Both,
Height = 1,
PlaceholderText = "type your message",
ReleaseFocusOnCommit = false,
HoldFocus = true,
}
}
},
loading = new LoadingSpinner(),
},
}
}
},
tabsArea = new TabsArea

View File

@ -14,7 +14,7 @@ namespace osu.Game.Overlays
private readonly Bindable<string> article = new Bindable<string>(null);
public NewsOverlay()
: base(OverlayColourScheme.Purple)
: base(OverlayColourScheme.Purple, false)
{
}

View File

@ -4,6 +4,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
namespace osu.Game.Overlays
{
@ -16,10 +17,16 @@ namespace osu.Game.Overlays
protected readonly LoadingLayer Loading;
private readonly Container content;
protected OnlineOverlay(OverlayColourScheme colourScheme)
protected OnlineOverlay(OverlayColourScheme colourScheme, bool requiresSignIn = true)
: base(colourScheme)
{
base.Content.AddRange(new Drawable[]
var mainContent = requiresSignIn
? new OnlineViewContainer($"Sign in to view the {Header.Title.Title}")
: new Container();
mainContent.RelativeSizeAxes = Axes.Both;
mainContent.AddRange(new Drawable[]
{
ScrollFlow = new OverlayScrollContainer
{
@ -43,6 +50,8 @@ namespace osu.Game.Overlays
},
Loading = new LoadingLayer(true)
});
base.Content.Add(mainContent);
}
}
}

View File

@ -144,8 +144,8 @@ namespace osu.Game.Overlays.Profile.Header
private void updateDisplay(User user)
{
hiddenDetailGlobal.Content = user?.Statistics?.Ranks.Global?.ToString("\\##,##0") ?? "-";
hiddenDetailCountry.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-";
hiddenDetailGlobal.Content = user?.Statistics?.GlobalRank?.ToString("\\##,##0") ?? "-";
hiddenDetailCountry.Content = user?.Statistics?.CountryRank?.ToString("\\##,##0") ?? "-";
}
}
}

View File

@ -176,8 +176,8 @@ namespace osu.Game.Overlays.Profile.Header
foreach (var scoreRankInfo in scoreRankInfos)
scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0;
detailGlobalRank.Content = user?.Statistics?.Ranks.Global?.ToString("\\##,##0") ?? "-";
detailCountryRank.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-";
detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToString("\\##,##0") ?? "-";
detailCountryRank.Content = user?.Statistics?.CountryRank?.ToString("\\##,##0") ?? "-";
rankGraph.Statistics.Value = user?.Statistics;
}

View File

@ -61,8 +61,7 @@ namespace osu.Game.Overlays
LoadComponentAsync(display, loaded =>
{
if (API.IsLoggedIn)
Loading.Hide();
Loading.Hide();
Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);

View File

@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.UI
public IEnumerable<string> GetAvailableResources() => throw new NotSupportedException();
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException();
public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();

View File

@ -8,11 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play;
@ -21,14 +24,20 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Cached(typeof(IPreviewTrackOwner))]
public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner
{
[Cached(typeof(IBindable<PlaylistItem>))]
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
public override bool DisallowExternalBeatmapRulesetChanges => true;
private Sample sampleStart;
private readonly ModSelectOverlay userModsSelectOverlay;
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
/// <summary>
/// A container that provides controls for selection of user mods.
/// This will be shown/hidden automatically when applicable.
/// </summary>
protected Drawable UserModsSection;
private Sample sampleStart;
/// <summary>
/// Any mods applied by/to the local user.
@ -53,9 +62,26 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected RoomSubScreen()
{
AddInternal(BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker
AddRangeInternal(new Drawable[]
{
SelectedItem = { BindTarget = SelectedItem }
BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker
{
SelectedItem = { BindTarget = SelectedItem }
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Depth = float.MinValue,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
IsValidMod = _ => false
}
},
});
}
@ -73,7 +99,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
base.LoadComplete();
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
SelectedItem.Value = Playlist.FirstOrDefault();
managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);
@ -81,6 +106,19 @@ namespace osu.Game.Screens.OnlinePlay.Match
UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods));
}
public override bool OnBackButton()
{
if (userModsSelectOverlay.State.Value == Visibility.Visible)
{
userModsSelectOverlay.Hide();
return true;
}
return base.OnBackButton();
}
protected void ShowUserModSelect() => userModsSelectOverlay.Show();
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
@ -120,17 +158,31 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
updateWorkingBeatmap();
if (SelectedItem.Value == null)
var selected = SelectedItem.Value;
if (selected == null)
return;
// Remove any user mods that are no longer allowed.
UserMods.Value = UserMods.Value
.Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType()))
.Where(m => selected.AllowedMods.Any(a => m.GetType() == a.GetType()))
.ToList();
UpdateMods();
Ruleset.Value = SelectedItem.Value.Ruleset.Value;
Ruleset.Value = selected.Ruleset.Value;
if (!selected.AllowedMods.Any())
{
UserModsSection?.Hide();
userModsSelectOverlay.Hide();
userModsSelectOverlay.IsValidMod = _ => false;
}
else
{
UserModsSection?.Show();
userModsSelectOverlay.IsValidMod = m => selected.AllowedMods.Any(a => a.GetType() == m.GetType());
}
}
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet) => Schedule(updateWorkingBeatmap);
@ -185,5 +237,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (track != null)
track.Looping = false;
}
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
{
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -13,7 +11,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class BeatmapSelectionControl : OnlinePlayComposite
public class BeatmapSelectionControl : RoomSubScreenComposite
{
[Resolved]
private MultiplayerMatchSubScreen matchSubScreen { get; set; }
@ -60,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true);
SelectedItem.BindValueChanged(_ => updateBeatmap(), true);
Host.BindValueChanged(host =>
{
if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true)
@ -70,12 +68,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}, true);
}
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e)
private void updateBeatmap()
{
if (Playlist.Any())
beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(Playlist.Single(), false, false);
else
if (SelectedItem.Value == null)
beatmapPanelContainer.Clear();
else
beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(SelectedItem.Value, false, false);
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
@ -16,7 +15,6 @@ using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
@ -43,11 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private ModSelectOverlay userModsSelectOverlay;
private MultiplayerMatchSettingsOverlay settingsOverlay;
private Drawable userModsSection;
private IBindable<bool> isConnected;
private readonly IBindable<bool> isConnected = new Bindable<bool>();
[CanBeNull]
private IDisposable readyClickOperation;
@ -155,7 +151,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
new BeatmapSelectionControl { RelativeSizeAxes = Axes.X }
}
},
userModsSection = new FillFlowContainer
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -176,7 +172,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = () => userModsSelectOverlay.Show()
Action = ShowUserModSelect,
},
new ModDisplay
{
@ -231,19 +227,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
new Dimension(GridSizeMode.AutoSize),
}
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
IsValidMod = _ => false
}
},
settingsOverlay = new MultiplayerMatchSettingsOverlay
{
RelativeSizeAxes = Axes.Both,
@ -269,14 +252,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true);
SelectedItem.BindTo(client.CurrentMatchPlayingItem);
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged);
client.LoadRequested += onLoadRequested;
client.RoomUpdated += onRoomUpdated;
isConnected = client.IsConnected.GetBoundCopy();
isConnected.BindTo(client.IsConnected);
isConnected.BindValueChanged(connected =>
{
if (!connected.NewValue)
@ -303,32 +287,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true;
}
if (userModsSelectOverlay.State.Value == Visibility.Visible)
{
userModsSelectOverlay.Hide();
return true;
}
return base.OnBackButton();
}
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e)
{
SelectedItem.Value = Playlist.FirstOrDefault();
if (SelectedItem.Value?.AllowedMods.Any() != true)
{
userModsSection.Hide();
userModsSelectOverlay.Hide();
userModsSelectOverlay.IsValidMod = _ => false;
}
else
{
userModsSection.Show();
userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType());
}
}
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedModSettingsUpdate;
@ -433,9 +394,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
modSettingChangeTracker?.Dispose();
}
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
{
}
}
}

View File

@ -89,6 +89,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(client.Room != null);
}
protected override void LoadComplete()
{
base.LoadComplete();
((IBindable<bool>)leaderboard.Expanded).BindTo(IsBreakTime);
}
protected override void StartGameplay()
{
// block base call, but let the server know we are ready to start.

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@ -35,9 +36,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
[Resolved]
private RulesetStore rulesets { get; set; }
private SpriteIcon crown;
private OsuSpriteText userRankText;
private ModDisplay userModsDisplay;
private StateDisplay userStateDisplay;
private SpriteIcon crown;
public ParticipantPanel(MultiplayerRoomUser user)
{
@ -119,12 +121,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
Text = user?.Username
},
new OsuSpriteText
userRankText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14),
Text = user?.CurrentModeRank != null ? $"#{user.CurrentModeRank}" : string.Empty
}
}
},
@ -162,6 +163,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance();
var currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if (Room.Host?.Equals(User) == true)
@ -171,11 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
Schedule(() =>
{
var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance();
userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList();
});
Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList());
}
public MenuItem[] ContextMenuItems

View File

@ -2,14 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users;
namespace osu.Game.Screens.OnlinePlay
{
/// <summary>
/// A <see cref="CompositeDrawable"/> that exposes bindables for <see cref="Room"/> properties.
/// </summary>
public class OnlinePlayComposite : CompositeDrawable
{
[Resolved(typeof(Room))]
@ -53,5 +58,23 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected Bindable<TimeSpan?> Duration { get; private set; }
/// <summary>
/// The currently selected item in the <see cref="RoomSubScreen"/>, or the first item from <see cref="Playlist"/>
/// if this <see cref="OnlinePlayComposite"/> is not within a <see cref="RoomSubScreen"/>.
/// </summary>
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
protected override void LoadComplete()
{
base.LoadComplete();
Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true);
}
protected virtual void UpdateSelectedItem()
{
SelectedItem.Value = Playlist.FirstOrDefault();
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -31,7 +32,12 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
private readonly Bindable<IReadOnlyList<Mod>> freeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
[CanBeNull]
[Resolved(CanBeNull = true)]
private IBindable<PlaylistItem> selectedItem { get; set; }
private readonly FreeModSelectOverlay freeModSelectOverlay;
private WorkingBeatmap initialBeatmap;
@ -45,7 +51,7 @@ namespace osu.Game.Screens.OnlinePlay
freeModSelectOverlay = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = freeMods },
SelectedMods = { BindTarget = FreeMods },
IsValidMod = IsValidFreeMod,
};
}
@ -66,15 +72,15 @@ namespace osu.Game.Screens.OnlinePlay
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods.
Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
Ruleset.BindValueChanged(onRulesetChanged);
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
{
freeMods.Value = Array.Empty<Mod>();
FreeMods.Value = Array.Empty<Mod>();
}
protected sealed override bool OnStart()
@ -90,7 +96,7 @@ namespace osu.Game.Screens.OnlinePlay
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.Clear();
item.AllowedMods.AddRange(freeMods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy()));
SelectItem(item);
return true;
@ -133,7 +139,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons()
{
var buttons = base.CreateFooterButtons().ToList();
buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = freeMods }, freeModSelectOverlay));
buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = FreeMods }, freeModSelectOverlay));
return buttons;
}

View File

@ -13,7 +13,9 @@ using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osuTK;
using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer;
namespace osu.Game.Screens.OnlinePlay.Playlists
@ -27,6 +29,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<long?> roomId { get; set; }
[Resolved(typeof(Room), nameof(Room.Playlist))]
private BindableList<PlaylistItem> playlist { get; set; }
private MatchSettingsOverlay settingsOverlay;
private MatchLeaderboard leaderboard;
@ -117,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new DrawableRoomPlaylistWithResults
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Playlist },
Items = { BindTarget = playlist },
SelectedItem = { BindTarget = SelectedItem },
RequestShowResults = item =>
{
@ -140,13 +145,55 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[] { new OverlinedHeader("Leaderboard"), },
new[]
{
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 10 },
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new PurpleTriangleButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DisplayUnrankedText = false,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
}
}
},
},
new Drawable[]
{
new OverlinedHeader("Leaderboard")
},
new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, },
new Drawable[] { new OverlinedHeader("Chat"), },
new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } }
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
@ -222,7 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
// Set the first playlist item.
// This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()).
Schedule(() => SelectedItem.Value = Playlist.FirstOrDefault());
Schedule(() => SelectedItem.Value = playlist.FirstOrDefault());
}
}, true);
}

View File

@ -56,6 +56,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.Clear();
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy()));
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match;
namespace osu.Game.Screens.OnlinePlay
{
/// <summary>
/// An <see cref="OnlinePlayComposite"/> with additional logic tracking the currently-selected <see cref="PlaylistItem"/> inside a <see cref="RoomSubScreen"/>.
/// </summary>
public class RoomSubScreenComposite : OnlinePlayComposite
{
[Resolved]
private IBindable<PlaylistItem> subScreenSelectedItem { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
subScreenSelectedItem.BindValueChanged(_ => UpdateSelectedItem(), true);
}
protected override void UpdateSelectedItem()
{
if (RoomID.Value == null)
{
// If the room hasn't been created yet, fall-back to the base logic.
base.UpdateSelectedItem();
return;
}
SelectedItem.Value = subScreenSelectedItem.Value;
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -16,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD
{
private readonly Cached sorting = new Cached();
public Bindable<bool> Expanded = new Bindable<bool>();
public GameplayLeaderboard()
{
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
@ -47,8 +50,7 @@ namespace osu.Game.Screens.Play.HUD
{
var drawable = new GameplayLeaderboardScore(user, isTracked)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Expanded = { BindTarget = Expanded },
};
base.Add(drawable);

View File

@ -20,16 +20,33 @@ namespace osu.Game.Screens.Play.HUD
{
public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore
{
public const float EXTENDED_WIDTH = 255f;
public const float EXTENDED_WIDTH = regular_width + top_player_left_width_extension;
private const float regular_width = 235f;
// a bit hand-wavy, but there's a lot of hard-coded paddings in each of the grid's internals.
private const float compact_width = 77.5f;
private const float top_player_left_width_extension = 20f;
public const float PANEL_HEIGHT = 35f;
public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear;
private const float panel_shear = 0.15f;
private const float rank_text_width = 35f;
private const float score_components_width = 85f;
private const float avatar_size = 25f;
private const double panel_transition_duration = 500;
private const double text_transition_duration = 200;
public Bindable<bool> Expanded = new Bindable<bool>();
private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText;
public BindableDouble TotalScore { get; } = new BindableDouble();
@ -63,8 +80,15 @@ namespace osu.Game.Screens.Play.HUD
private readonly bool trackedPlayer;
private Container mainFillContainer;
private Box centralFill;
private Container backgroundPaddingAdjustContainer;
private GridContainer gridContainer;
private Container scoreComponents;
/// <summary>
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
/// </summary>
@ -75,7 +99,8 @@ namespace osu.Game.Screens.Play.HUD
User = user;
this.trackedPlayer = trackedPlayer;
Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT);
AutoSizeAxes = Axes.X;
Height = PANEL_HEIGHT;
}
[BackgroundDependencyLoader]
@ -85,147 +110,167 @@ namespace osu.Game.Screens.Play.HUD
InternalChildren = new Drawable[]
{
mainFillContainer = new Container
new Container
{
Width = regular_width,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
Child = new Box
Margin = new MarginPadding { Left = top_player_left_width_extension },
Children = new Drawable[]
{
Alpha = 0.5f,
RelativeSizeAxes = Axes.Both,
}
},
new GridContainer
{
Width = regular_width,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 35f),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 85f),
},
Content = new[]
{
new Drawable[]
backgroundPaddingAdjustContainer = new Container
{
positionText = new OsuSpriteText
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold),
Shadow = false,
},
new Container
{
Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 },
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
mainFillContainer = new Container
{
new Container
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
Children = new Drawable[]
{
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
RelativeSizeAxes = Axes.Both,
Children = new[]
new Box
{
centralFill = new Box
{
Alpha = 0.5f,
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("3399cc"),
},
}
},
new FillFlowContainer
{
Padding = new MarginPadding { Left = SHEAR_WIDTH },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4f, 0f),
Children = new Drawable[]
{
avatarContainer = new CircularContainer
{
Masking = true,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(25f),
Children = new Drawable[]
{
new Box
{
Name = "Placeholder while avatar loads",
Alpha = 0.3f,
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray4,
}
}
},
usernameText = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 0.6f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Text = User?.Username,
Truncate = true,
Shadow = false,
}
}
},
}
},
new Container
{
Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f },
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Children = new Drawable[]
{
scoreText = new OsuSpriteText
{
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
},
accuracyText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1f, 0f),
Shadow = false,
},
comboText = new OsuSpriteText
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
Alpha = 0.5f,
RelativeSizeAxes = Axes.Both,
},
},
},
}
},
gridContainer = new GridContainer
{
RelativeSizeAxes = Axes.Y,
Width = compact_width, // will be updated by expanded state.
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, rank_text_width),
new Dimension(),
new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width),
},
Content = new[]
{
new Drawable[]
{
positionText = new OsuSpriteText
{
Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold),
Shadow = false,
},
new Container
{
Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 },
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
RelativeSizeAxes = Axes.Both,
Children = new[]
{
centralFill = new Box
{
Alpha = 0.5f,
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("3399cc"),
},
}
},
new FillFlowContainer
{
Padding = new MarginPadding { Left = SHEAR_WIDTH },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4f, 0f),
Children = new Drawable[]
{
avatarContainer = new CircularContainer
{
Masking = true,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(avatar_size),
Children = new Drawable[]
{
new Box
{
Name = "Placeholder while avatar loads",
Alpha = 0.3f,
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray4,
}
}
},
usernameText = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 0.6f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Text = User?.Username,
Truncate = true,
Shadow = false,
}
}
},
}
},
scoreComponents = new Container
{
Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f },
AlwaysPresent = true, // required to smoothly animate autosize after hidden early.
Masking = true,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Children = new Drawable[]
{
scoreText = new OsuSpriteText
{
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
},
accuracyText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1f, 0f),
Shadow = false,
},
comboText = new OsuSpriteText
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
},
},
}
}
}
}
}
}
},
};
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
@ -241,18 +286,43 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete();
updateState();
Expanded.BindValueChanged(changeExpandedState, true);
FinishTransforms(true);
}
private const double panel_transition_duration = 500;
private void changeExpandedState(ValueChangedEvent<bool> expanded)
{
scoreComponents.ClearTransforms();
if (expanded.NewValue)
{
gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.ResizeWidthTo(score_components_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint);
usernameText.FadeIn(panel_transition_duration, Easing.OutQuint);
}
else
{
gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint);
usernameText.FadeOut(text_transition_duration, Easing.OutQuint);
}
}
private void updateState()
{
bool widthExtension = false;
if (HasQuit.Value)
{
// we will probably want to display this in a better way once we have a design.
// and also show states other than quit.
mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic);
panelColour = Color4.Gray;
textColour = Color4.White;
return;
@ -260,22 +330,29 @@ namespace osu.Game.Screens.Play.HUD
if (scorePosition == 1)
{
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic);
widthExtension = true;
panelColour = Color4Extensions.FromHex("7fcc33");
textColour = Color4.White;
}
else if (trackedPlayer)
{
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic);
widthExtension = true;
panelColour = Color4Extensions.FromHex("ffd966");
textColour = Color4Extensions.FromHex("2e576b");
}
else
{
mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic);
panelColour = Color4Extensions.FromHex("3399cc");
textColour = Color4.White;
}
this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic);
}
public float SizeContainerLeftPadding
{
get => backgroundPaddingAdjustContainer.Padding.Left;
set => backgroundPaddingAdjustContainer.Padding = new MarginPadding { Left = value };
}
private Color4 panelColour
@ -287,8 +364,6 @@ namespace osu.Game.Screens.Play.HUD
}
}
private const double text_transition_duration = 200;
private Color4 textColour
{
set

View File

@ -339,7 +339,7 @@ namespace osu.Game.Screens.Play
{
HoldToQuit =
{
Action = performUserRequestedExit,
Action = () => PerformExit(true),
IsPaused = { BindTarget = GameplayClockContainer.IsPaused }
},
PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } },
@ -363,14 +363,14 @@ namespace osu.Game.Screens.Play
FailOverlay = new FailOverlay
{
OnRetry = Restart,
OnQuit = performUserRequestedExit,
OnQuit = () => PerformExit(true),
},
PauseOverlay = new PauseOverlay
{
OnResume = Resume,
Retries = RestartCount,
OnRetry = Restart,
OnQuit = performUserRequestedExit,
OnQuit = () => PerformExit(true),
},
new HotkeyExitOverlay
{
@ -379,7 +379,7 @@ namespace osu.Game.Screens.Play
if (!this.IsCurrentScreen()) return;
fadeOut(true);
PerformExit(true);
PerformExit(false);
},
},
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
@ -478,23 +478,47 @@ namespace osu.Game.Screens.Play
/// <summary>
/// Exits the <see cref="Player"/>.
/// </summary>
/// <param name="userRequested">
/// Whether the exit is requested by the user, or a higher-level game component.
/// Pausing is allowed only in the former case.
/// <param name="showDialogFirst">
/// Whether the pause or fail dialog should be shown before performing an exit.
/// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead.
/// </param>
protected void PerformExit(bool userRequested)
protected void PerformExit(bool showDialogFirst)
{
// if a restart has been requested, cancel any pending completion (user has shown intent to restart).
completionProgressDelegate?.Cancel();
ValidForResume = false;
// there is a chance that the exit was performed after the transition to results has started.
// we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen())
{
ValidForResume = false;
this.MakeCurrent();
return;
}
if (!this.IsCurrentScreen()) return;
bool pauseOrFailDialogVisible =
PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible;
if (userRequested)
performUserRequestedExit();
else
this.Exit();
if (showDialogFirst && !pauseOrFailDialogVisible)
{
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
if (ValidForResume && HasFailed)
{
failAnimation.FinishTransforms(true);
return;
}
// there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred.
if (pausingSupportedByCurrentState)
{
// in the case a dialog needs to be shown, attempt to pause and show it.
// this may fail (see internal checks in Pause()) but the fail cases are temporary, so don't fall through to Exit().
Pause();
return;
}
}
this.Exit();
}
private void performUserRequestedSkip()
@ -508,20 +532,6 @@ namespace osu.Game.Screens.Play
updateSampleDisabledState();
}
private void performUserRequestedExit()
{
if (ValidForResume && HasFailed && !FailOverlay.IsPresent)
{
failAnimation.FinishTransforms(true);
return;
}
if (canPause)
Pause();
else
this.Exit();
}
/// <summary>
/// Restart gameplay via a parent <see cref="PlayerLoader"/>.
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
@ -538,10 +548,7 @@ namespace osu.Game.Screens.Play
sampleRestart?.Play();
RestartRequested?.Invoke();
if (this.IsCurrentScreen())
PerformExit(true);
else
this.MakeCurrent();
PerformExit(false);
}
private ScheduledDelegate completionProgressDelegate;
@ -667,15 +674,17 @@ namespace osu.Game.Screens.Play
private double? lastPauseActionTime;
private bool canPause =>
/// <summary>
/// A set of conditionals which defines whether the current game state and configuration allows for
/// pausing to be attempted via <see cref="Pause"/>. If false, the game should generally exit if a user pause
/// is attempted.
/// </summary>
private bool pausingSupportedByCurrentState =>
// must pass basic screen conditions (beatmap loaded, instance allows pause)
LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume
// replays cannot be paused and exit immediately
&& !DrawableRuleset.HasReplayLoaded.Value
// cannot pause if we are already in a fail state
&& !HasFailed
// cannot pause if already paused (or in a cooldown state) unless we are in a resuming state.
&& (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive));
&& !HasFailed;
private bool pauseCooldownActive =>
lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown;
@ -690,7 +699,10 @@ namespace osu.Game.Screens.Play
public void Pause()
{
if (!canPause) return;
if (!pausingSupportedByCurrentState) return;
if (!IsResuming && pauseCooldownActive)
return;
if (IsResuming)
{
@ -809,14 +821,6 @@ namespace osu.Game.Screens.Play
return true;
}
// ValidForResume is false when restarting
if (ValidForResume)
{
if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value)
// still want to block if we are within the cooldown period and not already paused.
return true;
}
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable.
GameplayClockContainer?.StopUsingBeatmapClock();

View File

@ -44,7 +44,7 @@ namespace osu.Game.Screens.Select
Sort = sortMode.Value,
AllowConvertedBeatmaps = showConverted.Value,
Ruleset = ruleset.Value,
Collection = collectionDropdown?.Current.Value.Collection
Collection = collectionDropdown?.Current.Value?.Collection
};
if (!minimumStars.IsDefault)

View File

@ -165,9 +165,9 @@ namespace osu.Game.Skinning
public BindableNumber<double> Tempo => sampleContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable);
public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type);

View File

@ -176,10 +176,10 @@ namespace osu.Game.Skinning
public BindableNumber<double> Tempo => samplesContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
=> samplesContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
=> samplesContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type)

View File

@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests;
namespace osu.Game.Users
{
@ -178,6 +181,10 @@ namespace osu.Game.Users
private UserStatistics statistics;
/// <summary>
/// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> response).
/// Otherwise empty.
/// </summary>
[JsonProperty(@"statistics")]
public UserStatistics Statistics
{
@ -228,14 +235,14 @@ namespace osu.Game.Users
[JsonProperty("replays_watched_counts")]
public UserHistoryCount[] ReplaysWatchedCounts;
public class UserHistoryCount
{
[JsonProperty("start_date")]
public DateTime Date;
[JsonProperty("count")]
public long Count;
}
/// <summary>
/// All user statistics per ruleset's short name (in the case of a <see cref="GetUsersRequest"/> response).
/// Otherwise empty. Can be altered for testing purposes.
/// </summary>
// todo: this should likely be moved to a separate UserCompact class at some point.
[JsonProperty("statistics_rulesets")]
[CanBeNull]
public Dictionary<string, UserStatistics> RulesetsStatistics { get; set; }
public override string ToString() => Username;
@ -249,6 +256,14 @@ namespace osu.Game.Users
Id = 0
};
public bool Equals(User other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Id == other.Id;
}
public enum PlayStyle
{
[Description("Keyboard")]
@ -264,12 +279,13 @@ namespace osu.Game.Users
Touch,
}
public bool Equals(User other)
public class UserHistoryCount
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
[JsonProperty("start_date")]
public DateTime Date;
return Id == other.Id;
[JsonProperty("count")]
public long Count;
}
}
}

View File

@ -26,17 +26,24 @@ namespace osu.Game.Users
public int Progress;
}
[JsonProperty(@"pp")]
public decimal? PP;
[JsonProperty(@"global_rank")]
public int? GlobalRank;
[JsonProperty(@"pp_rank")] // the API sometimes only returns this value in condensed user responses
private int? rank
{
set => Ranks.Global = value;
}
public int? CountryRank;
[JsonProperty(@"rank")]
public UserRanks Ranks;
private UserRanks ranks
{
// eventually that will also become an own json property instead of reading from a `rank` object.
// see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53.
set => CountryRank = value.Country;
}
// populated via User model, as that's where the data currently lives.
public RankHistoryData RankHistory;
[JsonProperty(@"pp")]
public decimal? PP;
[JsonProperty(@"ranked_score")]
public long RankedScore;
@ -113,15 +120,12 @@ namespace osu.Game.Users
}
}
public struct UserRanks
#pragma warning disable 649
private struct UserRanks
{
[JsonProperty(@"global")]
public int? Global;
[JsonProperty(@"country")]
public int? Country;
}
public RankHistoryData RankHistory;
#pragma warning restore 649
}
}

View File

@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.215.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.220.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="Sentry" Version="3.0.1" />
<PackageReference Include="SharpCompress" Version="0.27.1" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.215.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.220.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -91,7 +91,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.215.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.220.0" />
<PackageReference Include="SharpCompress" Version="0.27.1" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />