1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-05 15:53:21 +08:00

Merge branch 'master' into remove-current-room

This commit is contained in:
smoogipoo 2021-08-20 16:23:36 +09:00
commit 610a162271
67 changed files with 1666 additions and 896 deletions

View File

@ -0,0 +1,34 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Benchmarks
{
public class BenchmarkMod : BenchmarkTest
{
private OsuModDoubleTime mod;
[Params(1, 10, 100)]
public int Times { get; set; }
[GlobalSetup]
public void GlobalSetup()
{
mod = new OsuModDoubleTime();
}
[Benchmark]
public int ModHashCode()
{
var hashCode = new HashCode();
for (int i = 0; i < Times; i++)
hashCode.Add(mod);
return hashCode.ToHashCode();
}
}
}

View File

@ -62,6 +62,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override bool InterpolateMovements => !disjointTrail; protected override bool InterpolateMovements => !disjointTrail;
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
protected override bool AvoidDrawingNearCursor => !disjointTrail;
protected override void Update() protected override void Update()
{ {

View File

@ -138,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected virtual bool InterpolateMovements => true; protected virtual bool InterpolateMovements => true;
protected virtual float IntervalMultiplier => 1.0f; protected virtual float IntervalMultiplier => 1.0f;
protected virtual bool AvoidDrawingNearCursor => false;
private Vector2? lastPosition; private Vector2? lastPosition;
private readonly InputResampler resampler = new InputResampler(); private readonly InputResampler resampler = new InputResampler();
@ -171,8 +172,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Vector2 direction = diff / distance; Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier; float interval = partSize.X / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
for (float d = interval; d < distance; d += interval) for (float d = interval; d < stopAt; d += interval)
{ {
lastPosition = pos1 + direction * d; lastPosition = pos1 + direction * d;
addPart(lastPosition.Value); addPart(lastPosition.Value);

View File

@ -1,56 +0,0 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public class BeatmapDifficultyCacheTest
{
[Test]
public void TestKeyEqualsWithDifferentModInstances()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
Assert.That(key1, Is.EqualTo(key2));
}
[Test]
public void TestKeyEqualsWithDifferentModOrder()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
Assert.That(key1, Is.EqualTo(key2));
}
[TestCase(1.3, DifficultyRating.Easy)]
[TestCase(1.993, DifficultyRating.Easy)]
[TestCase(1.998, DifficultyRating.Normal)]
[TestCase(2.4, DifficultyRating.Normal)]
[TestCase(2.693, DifficultyRating.Normal)]
[TestCase(2.698, DifficultyRating.Hard)]
[TestCase(3.5, DifficultyRating.Hard)]
[TestCase(3.993, DifficultyRating.Hard)]
[TestCase(3.997, DifficultyRating.Insane)]
[TestCase(5.0, DifficultyRating.Insane)]
[TestCase(5.292, DifficultyRating.Insane)]
[TestCase(5.297, DifficultyRating.Expert)]
[TestCase(6.2, DifficultyRating.Expert)]
[TestCase(6.493, DifficultyRating.Expert)]
[TestCase(6.498, DifficultyRating.ExpertPlus)]
[TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket);
}
}
}

View File

@ -0,0 +1,146 @@
// 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 System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Beatmaps
{
[HeadlessTest]
public class TestSceneBeatmapDifficultyCache : OsuTestScene
{
public const double BASE_STARS = 5.55;
private BeatmapSetInfo importedSet;
[Resolved]
private BeatmapManager beatmaps { get; set; }
private TestBeatmapDifficultyCache difficultyCache;
private IBindable<StarDifficulty?> starDifficultyBindable;
[BackgroundDependencyLoader]
private void load(OsuGameBase osu)
{
importedSet = ImportBeatmapTest.LoadQuickOszIntoOsu(osu).Result;
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup difficulty cache", () =>
{
SelectedMods.Value = Array.Empty<Mod>();
Child = difficultyCache = new TestBeatmapDifficultyCache();
starDifficultyBindable = difficultyCache.GetBindableDifficulty(importedSet.Beatmaps.First());
});
AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value?.Stars == BASE_STARS);
}
[Test]
public void TestStarDifficultyChangesOnModSettings()
{
OsuModDoubleTime dt = null;
AddStep("change selected mod to DT", () => SelectedMods.Value = new[] { dt = new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
AddUntilStep($"star difficulty -> {BASE_STARS + 1.5}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.5);
AddStep("change DT speed to 1.25", () => dt.SpeedChange.Value = 1.25);
AddUntilStep($"star difficulty -> {BASE_STARS + 1.25}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.25);
AddStep("change selected mod to NC", () => SelectedMods.Value = new[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } });
AddUntilStep($"star difficulty -> {BASE_STARS + 1.75}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.75);
}
[Test]
public void TestKeyEqualsWithDifferentModInstances()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyEqualsWithDifferentModOrder()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyDoesntEqualWithDifferentModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
Assert.That(key1, Is.Not.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyEqualWithMatchingModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[TestCase(1.3, DifficultyRating.Easy)]
[TestCase(1.993, DifficultyRating.Easy)]
[TestCase(1.998, DifficultyRating.Normal)]
[TestCase(2.4, DifficultyRating.Normal)]
[TestCase(2.693, DifficultyRating.Normal)]
[TestCase(2.698, DifficultyRating.Hard)]
[TestCase(3.5, DifficultyRating.Hard)]
[TestCase(3.993, DifficultyRating.Hard)]
[TestCase(3.997, DifficultyRating.Insane)]
[TestCase(5.0, DifficultyRating.Insane)]
[TestCase(5.292, DifficultyRating.Insane)]
[TestCase(5.297, DifficultyRating.Expert)]
[TestCase(6.2, DifficultyRating.Expert)]
[TestCase(6.493, DifficultyRating.Expert)]
[TestCase(6.498, DifficultyRating.ExpertPlus)]
[TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket);
}
private class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{
protected override Task<StarDifficulty> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
{
var rateAdjust = lookup.OrderedMods.OfType<ModRateAdjust>().SingleOrDefault();
if (rateAdjust != null)
return Task.FromResult(new StarDifficulty(BASE_STARS + rateAdjust.SpeedChange.Value, 0));
return Task.FromResult(new StarDifficulty(BASE_STARS, 0));
}
}
}
}

View File

@ -0,0 +1,72 @@
// 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 MessagePack;
using NUnit.Framework;
using osu.Game.Online;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
namespace osu.Game.Tests.Online
{
[TestFixture]
public class TestMultiplayerMessagePackSerialization
{
[Test]
public void TestSerialiseRoom()
{
var room = new MultiplayerRoom(1)
{
MatchState = new TeamVersusRoomState()
};
var serialized = MessagePackSerializer.Serialize(room);
var deserialized = MessagePackSerializer.Deserialize<MultiplayerRoom>(serialized);
Assert.IsTrue(deserialized.MatchState is TeamVersusRoomState);
}
[Test]
public void TestSerialiseUserStateExpected()
{
var state = new TeamVersusUserState();
var serialized = MessagePackSerializer.Serialize(typeof(MatchUserState), state);
var deserialized = MessagePackSerializer.Deserialize<MatchUserState>(serialized);
Assert.IsTrue(deserialized is TeamVersusUserState);
}
[Test]
public void TestSerialiseUnionFailsWithSingalR()
{
var state = new TeamVersusUserState();
// SignalR serialises using the actual type, rather than a base specification.
var serialized = MessagePackSerializer.Serialize(typeof(TeamVersusUserState), state);
// works with explicit type specified.
MessagePackSerializer.Deserialize<TeamVersusUserState>(serialized);
// fails with base (union) type.
Assert.Throws<MessagePackSerializationException>(() => MessagePackSerializer.Deserialize<MatchUserState>(serialized));
}
[Test]
public void TestSerialiseUnionSucceedsWithWorkaround()
{
var state = new TeamVersusUserState();
// SignalR serialises using the actual type, rather than a base specification.
var serialized = MessagePackSerializer.Serialize(typeof(TeamVersusUserState), state, SignalRUnionWorkaroundResolver.OPTIONS);
// works with explicit type specified.
MessagePackSerializer.Deserialize<TeamVersusUserState>(serialized);
// works with custom resolver.
var deserialized = MessagePackSerializer.Deserialize<MatchUserState>(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
Assert.IsTrue(deserialized is TeamVersusUserState);
}
}
}

View File

@ -78,6 +78,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
} }
[Test]
public void TestClampWhenSeekOutsideBeatmapBounds()
{
AddStep("stop clock", Clock.Stop);
AddStep("seek before start time", () => Clock.Seek(-1000));
AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0);
AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000));
AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength);
AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000));
AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0);
AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000));
AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
Beatmap.Disabled = false; Beatmap.Disabled = false;

View File

@ -3,7 +3,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.Gameplay
protected class OverlayTestPlayer : TestPlayer protected class OverlayTestPlayer : TestPlayer
{ {
public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value; public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value;
public new Bindable<bool> LocalUserPlaying => base.LocalUserPlaying;
} }
} }
} }

View File

@ -0,0 +1,131 @@
// 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.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneGameplayChatDisplay : MultiplayerTestScene
{
private GameplayChatDisplay chatDisplay;
[Cached(typeof(ILocalUserPlayInfo))]
private ILocalUserPlayInfo localUserInfo;
private readonly Bindable<bool> localUserPlaying = new Bindable<bool>();
private TextBox textBox => chatDisplay.ChildrenOfType<TextBox>().First();
public TestSceneGameplayChatDisplay()
{
var mockLocalUserInfo = new Mock<ILocalUserPlayInfo>();
mockLocalUserInfo.SetupGet(i => i.IsPlaying).Returns(localUserPlaying);
localUserInfo = mockLocalUserInfo.Object;
}
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
});
AddStep("expand", () => chatDisplay.Expanded.Value = true);
}
[Test]
public void TestCantClickWhenPlaying()
{
setLocalUserPlaying(true);
AddStep("attempt focus chat", () =>
{
InputManager.MoveMouseTo(textBox);
InputManager.Click(MouseButton.Left);
});
assertChatFocused(false);
}
[Test]
public void TestFocusDroppedWhenPlaying()
{
assertChatFocused(false);
AddStep("focus chat", () =>
{
InputManager.MoveMouseTo(textBox);
InputManager.Click(MouseButton.Left);
});
setLocalUserPlaying(true);
assertChatFocused(false);
// should still stay non-focused even after entering a new break section.
setLocalUserPlaying(false);
assertChatFocused(false);
}
[Test]
public void TestFocusOnTabKeyWhenExpanded()
{
setLocalUserPlaying(true);
assertChatFocused(false);
AddStep("press tab", () => InputManager.Key(Key.Tab));
assertChatFocused(true);
}
[Test]
public void TestFocusOnTabKeyWhenNotExpanded()
{
AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
AddStep("press tab", () => InputManager.Key(Key.Tab));
assertChatFocused(true);
AddUntilStep("is visible", () => chatDisplay.IsPresent);
AddStep("press enter", () => InputManager.Key(Key.Enter));
assertChatFocused(false);
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
}
[Test]
public void TestFocusToggleViaAction()
{
AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
AddStep("press tab", () => InputManager.Key(Key.Tab));
assertChatFocused(true);
AddUntilStep("is visible", () => chatDisplay.IsPresent);
AddStep("press tab", () => InputManager.Key(Key.Tab));
assertChatFocused(false);
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
}
private void assertChatFocused(bool isFocused) =>
AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);
private void setLocalUserPlaying(bool playing) =>
AddStep($"local user {(playing ? "playing" : "not playing")}", () => localUserPlaying.Value = playing);
}
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
{ {
protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager;
private RoomsContainer container; private RoomsContainer container;
@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add rooms", () => RoomManager.AddRooms(3)); AddStep("add rooms", () => RoomManager.AddRooms(3));
AddAssert("has 3 rooms", () => container.Rooms.Count == 3); AddAssert("has 3 rooms", () => container.Rooms.Count == 3);
AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault())); AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.FirstOrDefault()));
AddAssert("has 2 rooms", () => container.Rooms.Count == 2); AddAssert("has 2 rooms", () => container.Rooms.Count == 2);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));

View File

@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene public class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene
{ {
protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager;
private LoungeSubScreen loungeScreen; private LoungeSubScreen loungeScreen;

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 System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerPlayer : MultiplayerTestScene
{
private MultiplayerPlayer player;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("set beatmap", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
});
AddStep("initialise gameplay", () =>
{
Stack.Push(player = new MultiplayerPlayer(Client.CurrentMatchPlayingItem.Value, Client.Room?.Users.ToArray()));
});
}
[Test]
public void TestGameplay()
{
AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
}
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene
{ {
protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager; protected new TestRequestHandlingRoomManager RoomManager => (TestRequestHandlingRoomManager)base.RoomManager;
private LoungeSubScreen loungeScreen; private LoungeSubScreen loungeScreen;
@ -37,6 +37,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("add rooms", () => RoomManager.AddRooms(30)); AddStep("add rooms", () => RoomManager.AddRooms(30));
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
@ -53,6 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestScrollSelectedIntoView() public void TestScrollSelectedIntoView()
{ {
AddStep("add rooms", () => RoomManager.AddRooms(30)); AddStep("add rooms", () => RoomManager.AddRooms(30));
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));

View File

@ -11,7 +11,6 @@ using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -35,18 +34,6 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case CreateRoomScoreRequest createRoomScoreRequest:
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
return true;
}
return false;
};
} }
[SetUpSteps] [SetUpSteps]

View File

@ -1,65 +0,0 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Screens.Ranking.Expanded;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneStarRatingDisplay : OsuTestScene
{
[Test]
public void TestDisplay()
{
AddStep("load displays", () => Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ChildrenEnumerable = new[]
{
1.23,
2.34,
3.45,
4.56,
5.67,
6.78,
10.11,
}.Select(starRating => new StarRatingDisplay(new StarDifficulty(starRating, 0))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
})
});
}
[Test]
public void TestChangingStarRatingDisplay()
{
StarRatingDisplay starRating = null;
AddStep("load display", () => Child = starRating = new StarRatingDisplay(new StarDifficulty(5.55, 1))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3f),
});
AddRepeatStep("set random value", () =>
{
starRating.Current.Value = new StarDifficulty(RNG.NextDouble(0.0, 11.0), 1);
}, 10);
AddSliderStep("set exact stars", 0.0, 11.0, 5.55, d =>
{
if (starRating != null)
starRating.Current.Value = new StarDifficulty(d, 1);
});
}
}
}

View File

@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Settings
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddUntilStep("wait for load", () => panel.ChildrenOfType<GlobalKeyBindingsSection>().Any());
AddStep("Scroll to top", () => panel.ChildrenOfType<SettingsPanel.SettingsSectionsContainer>().First().ScrollToTop()); AddStep("Scroll to top", () => panel.ChildrenOfType<SettingsPanel.SettingsSectionsContainer>().First().ScrollToTop());
AddWaitStep("wait for scroll", 5); AddWaitStep("wait for scroll", 5);
} }

View File

@ -76,5 +76,23 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("restore default", () => sliderBar.Current.SetDefault()); AddStep("restore default", () => sliderBar.Current.SetDefault());
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
} }
[Test]
public void TestWarningTextVisibility()
{
SettingsNumberBox numberBox = null;
AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox());
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<SettingsNoticeText>().Any());
AddStep("set warning text", () => numberBox.WarningText = "this is a warning!");
AddAssert("warning text created", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
AddStep("unset warning text", () => numberBox.WarningText = default);
AddAssert("warning text hidden", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 0);
AddStep("set warning text again", () => numberBox.WarningText = "another warning!");
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
}
} }
} }

View File

@ -4,6 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays; using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
@ -11,27 +12,39 @@ namespace osu.Game.Tests.Visual.Settings
[TestFixture] [TestFixture]
public class TestSceneSettingsPanel : OsuTestScene public class TestSceneSettingsPanel : OsuTestScene
{ {
private readonly SettingsPanel settings; private SettingsPanel settings;
private readonly DialogOverlay dialogOverlay; private DialogOverlay dialogOverlay;
public TestSceneSettingsPanel() [SetUpSteps]
public void SetUpSteps()
{ {
settings = new SettingsOverlay AddStep("create settings", () =>
{ {
State = { Value = Visibility.Visible } settings?.Expire();
};
Add(dialogOverlay = new DialogOverlay Add(settings = new SettingsOverlay
{ {
Depth = -1 State = { Value = Visibility.Visible }
});
}); });
} }
[Test]
public void ToggleVisibility()
{
AddWaitStep("wait some", 5);
AddToggleStep("toggle editor visibility", visible => settings.ToggleVisibility());
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Dependencies.Cache(dialogOverlay); Add(dialogOverlay = new DialogOverlay
{
Depth = -1
});
Add(settings); Dependencies.Cache(dialogOverlay);
} }
} }
} }

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -65,6 +66,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("show", () => { infoWedge.Show(); }); AddStep("show", () => { infoWedge.Show(); });
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
{
foreach (var hasCurrentValue in infoWedge.Info.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
hasCurrentValue.Current.Value = new StarDifficulty(v, 0);
});
foreach (var rulesetInfo in rulesets.AvailableRulesets) foreach (var rulesetInfo in rulesets.AvailableRulesets)
{ {
var instance = rulesetInfo.CreateInstance(); var instance = rulesetInfo.CreateInstance();

View File

@ -0,0 +1,71 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneStarRatingDisplay : OsuTestScene
{
[TestCase(StarRatingDisplaySize.Regular)]
[TestCase(StarRatingDisplaySize.Small)]
public void TestDisplay(StarRatingDisplaySize size)
{
AddStep("load displays", () =>
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(2f),
Direction = FillDirection.Horizontal,
ChildrenEnumerable = Enumerable.Range(0, 15).Select(i => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(2f),
Direction = FillDirection.Vertical,
ChildrenEnumerable = Enumerable.Range(0, 10).Select(j => new StarRatingDisplay(new StarDifficulty(i * (i >= 11 ? 25f : 1f) + j * 0.1f, 0), size)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}),
})
};
});
}
[Test]
public void TestSpectrum()
{
StarRatingDisplay starRating = null;
AddStep("load display", () => Child = starRating = new StarRatingDisplay(new StarDifficulty(5.55, 1), animated: true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3f),
});
AddRepeatStep("set random value", () =>
{
starRating.Current.Value = new StarDifficulty(RNG.NextDouble(0.0, 11.0), 1);
}, 10);
AddSliderStep("set exact stars", 0.0, 11.0, 5.55, d =>
{
if (starRating != null)
starRating.Current.Value = new StarDifficulty(d, 1);
});
}
}
}

View File

@ -14,6 +14,7 @@ using osu.Framework.Lists;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -56,12 +57,28 @@ namespace osu.Game.Beatmaps
[Resolved] [Resolved]
private Bindable<IReadOnlyList<Mod>> currentMods { get; set; } private Bindable<IReadOnlyList<Mod>> currentMods { get; set; }
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedModSettingsChange;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
currentRuleset.BindValueChanged(_ => updateTrackedBindables()); currentRuleset.BindValueChanged(_ => updateTrackedBindables());
currentMods.BindValueChanged(_ => updateTrackedBindables(), true);
currentMods.BindValueChanged(mods =>
{
modSettingChangeTracker?.Dispose();
updateTrackedBindables();
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += _ =>
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = Scheduler.AddDelayed(updateTrackedBindables, 100);
};
}, true);
} }
/// <summary> /// <summary>
@ -84,7 +101,7 @@ namespace osu.Game.Beatmaps
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination. /// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The bindable will not update to follow the currently-selected ruleset and mods. /// The bindable will not update to follow the currently-selected ruleset and mods or its settings.
/// </remarks> /// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param> /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param> /// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
@ -275,6 +292,8 @@ namespace osu.Game.Beatmaps
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
modSettingChangeTracker?.Dispose();
cancelTrackedBindableUpdate(); cancelTrackedBindableUpdate();
updateScheduler?.Dispose(); updateScheduler?.Dispose();
} }
@ -297,7 +316,7 @@ namespace osu.Game.Beatmaps
public bool Equals(DifficultyCacheLookup other) public bool Equals(DifficultyCacheLookup other)
=> Beatmap.ID == other.Beatmap.ID => Beatmap.ID == other.Beatmap.ID
&& Ruleset.ID == other.Ruleset.ID && Ruleset.ID == other.Ruleset.ID
&& OrderedMods.Select(m => m.Acronym).SequenceEqual(other.OrderedMods.Select(m => m.Acronym)); && OrderedMods.SequenceEqual(other.OrderedMods);
public override int GetHashCode() public override int GetHashCode()
{ {
@ -307,7 +326,7 @@ namespace osu.Game.Beatmaps
hashCode.Add(Ruleset.ID); hashCode.Add(Ruleset.ID);
foreach (var mod in OrderedMods) foreach (var mod in OrderedMods)
hashCode.Add(mod.Acronym); hashCode.Add(mod);
return hashCode.ToHashCode(); return hashCode.ToHashCode();
} }

View File

@ -0,0 +1,168 @@
// 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.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables
{
/// <summary>
/// A pill that displays the star rating of a beatmap.
/// </summary>
public class StarRatingDisplay : CompositeDrawable, IHasCurrentValue<StarDifficulty>
{
private readonly bool animated;
private readonly Box background;
private readonly SpriteIcon starIcon;
private readonly OsuSpriteText starsText;
private readonly BindableWithCurrent<StarDifficulty> current = new BindableWithCurrent<StarDifficulty>();
public Bindable<StarDifficulty> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly Bindable<double> displayedStars = new BindableDouble();
/// <summary>
/// The currently displayed stars of this display wrapped in a bindable.
/// This bindable gets transformed on change rather than instantaneous, if animation is enabled.
/// </summary>
public IBindable<double> DisplayedStars => displayedStars;
[Resolved]
private OsuColour colours { get; set; }
[Resolved(canBeNull: true)]
private OverlayColourProvider colourProvider { get; set; }
/// <summary>
/// Creates a new <see cref="StarRatingDisplay"/> using an already computed <see cref="StarDifficulty"/>.
/// </summary>
/// <param name="starDifficulty">The already computed <see cref="StarDifficulty"/> to display.</param>
/// <param name="size">The size of the star rating display.</param>
/// <param name="animated">Whether the star rating display will perform transforms on change rather than updating instantaneously.</param>
public StarRatingDisplay(StarDifficulty starDifficulty, StarRatingDisplaySize size = StarRatingDisplaySize.Regular, bool animated = false)
{
this.animated = animated;
Current.Value = starDifficulty;
AutoSizeAxes = Axes.Both;
MarginPadding margin = default;
switch (size)
{
case StarRatingDisplaySize.Small:
margin = new MarginPadding { Horizontal = 7f };
break;
case StarRatingDisplaySize.Range:
margin = new MarginPadding { Horizontal = 8f };
break;
case StarRatingDisplaySize.Regular:
margin = new MarginPadding { Horizontal = 8f, Vertical = 2f };
break;
}
InternalChild = new CircularContainer
{
Masking = true,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Margin = margin,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 3f),
new Dimension(GridSizeMode.AutoSize, minSize: 25f),
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
{
new[]
{
starIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Star,
Size = new Vector2(8f),
},
Empty(),
starsText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 1.5f },
// todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f
// see https://github.com/ppy/osu-framework/issues/3271.
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold),
Shadow = false,
},
}
}
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(c =>
{
if (animated)
this.TransformBindableTo(displayedStars, c.NewValue.Stars, 750, Easing.OutQuint);
else
displayedStars.Value = c.NewValue.Stars;
});
displayedStars.Value = Current.Value.Stars;
displayedStars.BindValueChanged(s =>
{
starsText.Text = s.NewValue.ToLocalisableString("0.00");
background.Colour = colours.ForStarDifficulty(s.NewValue);
starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
}, true);
}
}
public enum StarRatingDisplaySize
{
Small,
Range,
Regular,
}
}

View File

@ -22,9 +22,14 @@ namespace osu.Game.Graphics.UserInterface
public void TakeFocus() public void TakeFocus()
{ {
if (allowImmediateFocus) GetContainingInputManager().ChangeFocus(this); if (!allowImmediateFocus)
return;
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this), false);
} }
public new void KillFocus() => base.KillFocus();
public bool HoldFocus public bool HoldFocus
{ {
get => allowImmediateFocus && focus; get => allowImmediateFocus && focus;

View File

@ -6,6 +6,7 @@ using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
@ -57,18 +58,13 @@ namespace osu.Game.Graphics.UserInterface
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Colour = GlowColour, Colour = GlowColour.Opacity(0),
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Radius = 10, Radius = 10,
Roundness = 8, Roundness = 8,
}; };
} }
protected override void LoadComplete()
{
FadeEdgeEffectTo(0);
}
private bool glowing; private bool glowing;
public bool Glowing public bool Glowing
@ -153,7 +149,7 @@ namespace osu.Game.Graphics.UserInterface
glowColour = value; glowColour = value;
var effect = EdgeEffect; var effect = EdgeEffect;
effect.Colour = value; effect.Colour = Glowing ? value : value.Opacity(0);
EdgeEffect = effect; EdgeEffect = effect;
} }
} }

View File

@ -36,6 +36,7 @@ namespace osu.Game.Graphics.UserInterface
public Color4 BackgroundColour public Color4 BackgroundColour
{ {
get => backgroundColour ?? Color4.White;
set set
{ {
backgroundColour = value; backgroundColour = value;

View File

@ -69,6 +69,7 @@ namespace osu.Game.Graphics.UserInterface
BackgroundColour = Color4.Black.Opacity(0.5f); BackgroundColour = Color4.Black.Opacity(0.5f);
MaskingContainer.CornerRadius = corner_radius; MaskingContainer.CornerRadius = corner_radius;
Alpha = 0;
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
ItemsContainer.Padding = new MarginPadding(5); ItemsContainer.Padding = new MarginPadding(5);
@ -94,9 +95,11 @@ namespace osu.Game.Graphics.UserInterface
protected override void AnimateClose() protected override void AnimateClose()
{ {
this.FadeOut(300, Easing.OutQuint);
if (wasOpened) if (wasOpened)
{
this.FadeOut(300, Easing.OutQuint);
sampleClose?.Play(); sampleClose?.Play();
}
} }
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring

View File

@ -90,6 +90,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward), new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward),
new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward), new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus),
}; };
public IEnumerable<KeyBinding> SongSelectKeyBindings => new[] public IEnumerable<KeyBinding> SongSelectKeyBindings => new[]
@ -280,5 +281,8 @@ namespace osu.Game.Input.Bindings
[Description("Seek replay backward")] [Description("Seek replay backward")]
SeekReplayBackward, SeekReplayBackward,
[Description("Toggle chat focus")]
ToggleChatFocus
} }
} }

View File

@ -305,9 +305,11 @@ namespace osu.Game.Online.API
{ {
req.Perform(this); req.Perform(this);
if (req.CompletionState != APIRequestCompletionState.Completed)
return false;
// we could still be in initialisation, at which point we don't want to say we're Online yet. // we could still be in initialisation, at which point we don't want to say we're Online yet.
if (IsLoggedIn) state.Value = APIState.Online; if (IsLoggedIn) state.Value = APIState.Online;
failureCount = 0; failureCount = 0;
return true; return true;
} }
@ -381,7 +383,7 @@ namespace osu.Game.Online.API
} }
} }
public bool IsLoggedIn => localUser.Value.Id > 1; public bool IsLoggedIn => localUser.Value.Id > 1; // TODO: should this also be true if attempting to connect?
public void Queue(APIRequest request) public void Queue(APIRequest request)
{ {

View File

@ -84,7 +84,7 @@ namespace osu.Game.Online.API
/// The state of this request, from an outside perspective. /// The state of this request, from an outside perspective.
/// This is used to ensure correct notification events are fired. /// This is used to ensure correct notification events are fired.
/// </summary> /// </summary>
private APIRequestCompletionState completionState; public APIRequestCompletionState CompletionState { get; private set; }
public void Perform(IAPIProvider api) public void Perform(IAPIProvider api)
{ {
@ -127,10 +127,10 @@ namespace osu.Game.Online.API
{ {
lock (completionStateLock) lock (completionStateLock)
{ {
if (completionState != APIRequestCompletionState.Waiting) if (CompletionState != APIRequestCompletionState.Waiting)
return; return;
completionState = APIRequestCompletionState.Completed; CompletionState = APIRequestCompletionState.Completed;
} }
if (API == null) if (API == null)
@ -143,10 +143,10 @@ namespace osu.Game.Online.API
{ {
lock (completionStateLock) lock (completionStateLock)
{ {
if (completionState != APIRequestCompletionState.Waiting) if (CompletionState != APIRequestCompletionState.Waiting)
return; return;
completionState = APIRequestCompletionState.Failed; CompletionState = APIRequestCompletionState.Failed;
} }
if (API == null) if (API == null)
@ -161,7 +161,7 @@ namespace osu.Game.Online.API
{ {
lock (completionStateLock) lock (completionStateLock)
{ {
if (completionState != APIRequestCompletionState.Waiting) if (CompletionState != APIRequestCompletionState.Waiting)
return; return;
WebRequest?.Abort(); WebRequest?.Abort();
@ -200,7 +200,7 @@ namespace osu.Game.Online.API
get get
{ {
lock (completionStateLock) lock (completionStateLock)
return completionState == APIRequestCompletionState.Failed; return CompletionState == APIRequestCompletionState.Failed;
} }
} }

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat;
@ -22,7 +23,7 @@ namespace osu.Game.Online.Chat
{ {
public readonly Bindable<Channel> Channel = new Bindable<Channel>(); public readonly Bindable<Channel> Channel = new Bindable<Channel>();
private readonly FocusedTextBox textbox; protected readonly ChatTextBox Textbox;
protected ChannelManager ChannelManager; protected ChannelManager ChannelManager;
@ -30,6 +31,8 @@ namespace osu.Game.Online.Chat
private readonly bool postingTextbox; private readonly bool postingTextbox;
protected readonly Box Background;
private const float textbox_height = 30; private const float textbox_height = 30;
/// <summary> /// <summary>
@ -44,7 +47,7 @@ namespace osu.Game.Online.Chat
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Box Background = new Box
{ {
Colour = Color4.Black, Colour = Color4.Black,
Alpha = 0.8f, Alpha = 0.8f,
@ -54,7 +57,7 @@ namespace osu.Game.Online.Chat
if (postingTextbox) if (postingTextbox)
{ {
AddInternal(textbox = new FocusedTextBox AddInternal(Textbox = new ChatTextBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = textbox_height, Height = textbox_height,
@ -65,7 +68,7 @@ namespace osu.Game.Online.Chat
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
}); });
textbox.OnCommit += postMessage; Textbox.OnCommit += postMessage;
} }
Channel.BindValueChanged(channelChanged); Channel.BindValueChanged(channelChanged);
@ -82,7 +85,7 @@ namespace osu.Game.Online.Chat
private void postMessage(TextBox sender, bool newtext) private void postMessage(TextBox sender, bool newtext)
{ {
var text = textbox.Text.Trim(); var text = Textbox.Text.Trim();
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
return; return;
@ -92,7 +95,7 @@ namespace osu.Game.Online.Chat
else else
ChannelManager?.PostMessage(text, target: Channel.Value); ChannelManager?.PostMessage(text, target: Channel.Value);
textbox.Text = string.Empty; Textbox.Text = string.Empty;
} }
protected virtual ChatLine CreateMessage(Message message) => new StandAloneMessage(message); protected virtual ChatLine CreateMessage(Message message) => new StandAloneMessage(message);
@ -110,6 +113,25 @@ namespace osu.Game.Online.Chat
AddInternal(drawableChannel); AddInternal(drawableChannel);
} }
public class ChatTextBox : FocusedTextBox
{
protected override void LoadComplete()
{
base.LoadComplete();
BackgroundUnfocused = new Color4(10, 10, 10, 10);
BackgroundFocused = new Color4(10, 10, 10, 255);
}
protected override void OnFocusLost(FocusLostEvent e)
{
base.OnFocusLost(e);
FocusLost?.Invoke();
}
public Action FocusLost;
}
public class StandAloneDrawableChannel : DrawableChannel public class StandAloneDrawableChannel : DrawableChannel
{ {
public Func<Message, ChatLine> CreateChatLineAction; public Func<Message, ChatLine> CreateChatLineAction;

View File

@ -148,7 +148,12 @@ namespace osu.Game.Online
}); });
if (RuntimeInfo.SupportsJIT && preferMessagePack) if (RuntimeInfo.SupportsJIT && preferMessagePack)
builder.AddMessagePackProtocol(); {
builder.AddMessagePackProtocol(options =>
{
options.SerializerOptions = SignalRUnionWorkaroundResolver.OPTIONS;
});
}
else else
{ {
// eventually we will precompile resolvers for messagepack, but this isn't working currently // eventually we will precompile resolvers for messagepack, but this isn't working currently

View File

@ -15,9 +15,9 @@ namespace osu.Game.Online.Multiplayer
/// </summary> /// </summary>
[Serializable] [Serializable]
[MessagePackObject] [MessagePackObject]
[Union(0, typeof(TeamVersusRoomState))] [Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
// TODO: this will need to be abstract or interface when/if we get messagepack working. for now it isn't as it breaks json serialisation. // TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead).
public class MatchRoomState public abstract class MatchRoomState
{ {
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using MessagePack; using MessagePack;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
namespace osu.Game.Online.Multiplayer namespace osu.Game.Online.Multiplayer
{ {
@ -11,6 +12,7 @@ namespace osu.Game.Online.Multiplayer
/// </summary> /// </summary>
[Serializable] [Serializable]
[MessagePackObject] [MessagePackObject]
[Union(0, typeof(ChangeTeamRequest))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
public abstract class MatchUserRequest public abstract class MatchUserRequest
{ {
} }

View File

@ -15,9 +15,9 @@ namespace osu.Game.Online.Multiplayer
/// </summary> /// </summary>
[Serializable] [Serializable]
[MessagePackObject] [MessagePackObject]
[Union(0, typeof(TeamVersusUserState))] [Union(0, typeof(TeamVersusUserState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
// TODO: this will need to be abstract or interface when/if we get messagepack working. for now it isn't as it breaks json serialisation. // TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead).
public class MatchUserState public abstract class MatchUserState
{ {
} }
} }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Online.Multiplayer
{ {
// Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization.
// More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code.
connector = api.GetHubConnector(nameof(OnlineMultiplayerClient), endpoint, false); connector = api.GetHubConnector(nameof(OnlineMultiplayerClient), endpoint);
if (connector != null) if (connector != null)
{ {

View File

@ -0,0 +1,61 @@
// 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 System.Collections.Generic;
using MessagePack;
using MessagePack.Formatters;
using MessagePack.Resolvers;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
namespace osu.Game.Online
{
/// <summary>
/// Handles SignalR being unable to comprehend [Union] types correctly by redirecting to a known base (union) type.
/// See https://github.com/dotnet/aspnetcore/issues/7298.
/// </summary>
public class SignalRUnionWorkaroundResolver : IFormatterResolver
{
public static readonly MessagePackSerializerOptions OPTIONS =
MessagePackSerializerOptions.Standard.WithResolver(new SignalRUnionWorkaroundResolver());
private static readonly Dictionary<Type, IMessagePackFormatter> formatter_map = new Dictionary<Type, IMessagePackFormatter>
{
{ typeof(TeamVersusUserState), new TypeRedirectingFormatter<TeamVersusUserState, MatchUserState>() },
{ typeof(TeamVersusRoomState), new TypeRedirectingFormatter<TeamVersusRoomState, MatchRoomState>() },
{ typeof(ChangeTeamRequest), new TypeRedirectingFormatter<ChangeTeamRequest, MatchUserRequest>() },
// These should not be required. The fallback should work. But something is weird with the way caching is done.
// For future adventurers, I would not advise looking into this further. It's likely not worth the effort.
{ typeof(MatchUserState), new TypeRedirectingFormatter<MatchUserState, MatchUserState>() },
{ typeof(MatchRoomState), new TypeRedirectingFormatter<MatchRoomState, MatchRoomState>() },
{ typeof(MatchUserRequest), new TypeRedirectingFormatter<MatchUserRequest, MatchUserRequest>() },
{ typeof(MatchServerEvent), new TypeRedirectingFormatter<MatchServerEvent, MatchServerEvent>() },
};
public IMessagePackFormatter<T> GetFormatter<T>()
{
if (formatter_map.TryGetValue(typeof(T), out var formatter))
return (IMessagePackFormatter<T>)formatter;
return StandardResolver.Instance.GetFormatter<T>();
}
public class TypeRedirectingFormatter<TActual, TBase> : IMessagePackFormatter<TActual>
{
private readonly IMessagePackFormatter<TBase> baseFormatter;
public TypeRedirectingFormatter()
{
baseFormatter = StandardResolver.Instance.GetFormatter<TBase>();
}
public void Serialize(ref MessagePackWriter writer, TActual value, MessagePackSerializerOptions options) =>
baseFormatter.Serialize(ref writer, (TBase)(object)value, StandardResolver.Options);
public TActual Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) =>
(TActual)(object)baseFormatter.Deserialize(ref reader, StandardResolver.Options);
}
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -38,12 +39,12 @@ namespace osu.Game.Overlays
current.ValueChanged += _ => UpdateState(); current.ValueChanged += _ => UpdateState();
current.DefaultChanged += _ => UpdateState(); current.DefaultChanged += _ => UpdateState();
current.DisabledChanged += _ => UpdateState(); current.DisabledChanged += _ => UpdateState();
UpdateState();
if (IsLoaded)
UpdateState();
} }
} }
private Color4 buttonColour;
private bool hovering; private bool hovering;
public RestoreDefaultValueButton() public RestoreDefaultValueButton()
@ -58,12 +59,11 @@ namespace osu.Game.Overlays
private void load(OsuColour colour) private void load(OsuColour colour)
{ {
BackgroundColour = colour.Yellow; BackgroundColour = colour.Yellow;
buttonColour = colour.Yellow;
Content.Width = 0.33f; Content.Width = 0.33f;
Content.CornerRadius = 3; Content.CornerRadius = 3;
Content.EdgeEffect = new EdgeEffectParameters Content.EdgeEffect = new EdgeEffectParameters
{ {
Colour = buttonColour.Opacity(0.1f), Colour = BackgroundColour.Opacity(0.1f),
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Radius = 2, Radius = 2,
}; };
@ -81,7 +81,10 @@ namespace osu.Game.Overlays
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
UpdateState();
// avoid unnecessary transforms on first display.
Alpha = currentAlpha;
Background.Colour = currentColour;
} }
public LocalisableString TooltipText => "revert to default"; public LocalisableString TooltipText => "revert to default";
@ -101,14 +104,16 @@ namespace osu.Game.Overlays
public void UpdateState() => Scheduler.AddOnce(updateState); public void UpdateState() => Scheduler.AddOnce(updateState);
private float currentAlpha => current.IsDefault ? 0f : hovering && !current.Disabled ? 1f : 0.65f;
private ColourInfo currentColour => current.Disabled ? Color4.Gray : BackgroundColour;
private void updateState() private void updateState()
{ {
if (current == null) if (current == null)
return; return;
this.FadeTo(current.IsDefault ? 0f : this.FadeTo(currentAlpha, 200, Easing.OutQuint);
hovering && !current.Disabled ? 1f : 0.65f, 200, Easing.OutQuint); Background.FadeColour(currentColour, 200, Easing.OutQuint);
this.FadeColour(current.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint);
} }
} }
} }

View File

@ -21,17 +21,26 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
private SettingsDropdown<string> dropdown; private SettingsDropdown<string> dropdown;
protected override void Dispose(bool isDisposing) [BackgroundDependencyLoader]
private void load()
{ {
base.Dispose(isDisposing); Children = new Drawable[]
if (audio != null)
{ {
audio.OnNewDevice -= onDeviceChanged; dropdown = new AudioDeviceSettingsDropdown
audio.OnLostDevice -= onDeviceChanged; {
} Keywords = new[] { "speaker", "headphone", "output" }
}
};
updateItems();
audio.OnNewDevice += onDeviceChanged;
audio.OnLostDevice += onDeviceChanged;
dropdown.Current = audio.AudioDevice;
} }
private void onDeviceChanged(string name) => updateItems();
private void updateItems() private void updateItems()
{ {
var deviceItems = new List<string> { string.Empty }; var deviceItems = new List<string> { string.Empty };
@ -50,26 +59,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
dropdown.Items = deviceItems.Distinct().ToList(); dropdown.Items = deviceItems.Distinct().ToList();
} }
private void onDeviceChanged(string name) => updateItems(); protected override void Dispose(bool isDisposing)
protected override void LoadComplete()
{ {
base.LoadComplete(); base.Dispose(isDisposing);
Children = new Drawable[] if (audio != null)
{ {
dropdown = new AudioDeviceSettingsDropdown audio.OnNewDevice -= onDeviceChanged;
{ audio.OnLostDevice -= onDeviceChanged;
Keywords = new[] { "speaker", "headphone", "output" } }
}
};
updateItems();
dropdown.Current = audio.AudioDevice;
audio.OnNewDevice += onDeviceChanged;
audio.OnLostDevice += onDeviceChanged;
} }
private class AudioDeviceSettingsDropdown : SettingsDropdown<string> private class AudioDeviceSettingsDropdown : SettingsDropdown<string>

View File

@ -98,8 +98,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
AutoSizeDuration = transition_duration,
AutoSizeEasing = Easing.OutQuint,
Masking = true, Masking = true,
Children = new[] Children = new[]
{ {
@ -176,13 +174,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingMode.BindValueChanged(mode => scalingMode.BindValueChanged(mode =>
{ {
scalingSettings.ClearTransforms(); scalingSettings.ClearTransforms();
scalingSettings.AutoSizeAxes = mode.NewValue != ScalingMode.Off ? Axes.Y : Axes.None; scalingSettings.AutoSizeDuration = transition_duration;
scalingSettings.AutoSizeEasing = Easing.OutQuint;
if (mode.NewValue == ScalingMode.Off) updateScalingModeVisibility();
scalingSettings.ResizeHeightTo(0, transition_duration, Easing.OutQuint); });
scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything); // initial update bypasses transforms
}, true); updateScalingModeVisibility();
void updateResolutionDropdown() void updateResolutionDropdown()
{ {
@ -191,6 +190,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
else else
resolutionDropdown.Hide(); resolutionDropdown.Hide();
} }
void updateScalingModeVisibility()
{
if (scalingMode.Value == ScalingMode.Off)
scalingSettings.ResizeHeightTo(0, transition_duration, Easing.OutQuint);
scalingSettings.AutoSizeAxes = scalingMode.Value != ScalingMode.Off ? Axes.Y : Axes.None;
scalingSettings.ForEach(s => s.TransferValueOnCommit = scalingMode.Value == ScalingMode.Everything);
}
} }
private void bindPreviewEvent(Bindable<float> bindable) private void bindPreviewEvent(Bindable<float> bindable)

View File

@ -65,7 +65,7 @@ namespace osu.Game.Overlays.Settings
{ {
set set
{ {
bool hasValue = string.IsNullOrWhiteSpace(value.ToString()); bool hasValue = !string.IsNullOrWhiteSpace(value.ToString());
if (warningText == null) if (warningText == null)
{ {
@ -76,7 +76,7 @@ namespace osu.Game.Overlays.Settings
FlowContent.Add(warningText = new SettingsNoticeText(colours) { Margin = new MarginPadding { Bottom = 5 } }); FlowContent.Add(warningText = new SettingsNoticeText(colours) { Margin = new MarginPadding { Bottom = 5 } });
} }
warningText.Alpha = hasValue ? 0 : 1; warningText.Alpha = hasValue ? 1 : 0;
warningText.Text = value.ToString(); // TODO: Remove ToString() call after TextFlowContainer supports localisation (see https://github.com/ppy/osu-framework/issues/4636). warningText.Text = value.ToString(); // TODO: Remove ToString() call after TextFlowContainer supports localisation (see https://github.com/ppy/osu-framework/issues/4636).
} }
} }
@ -93,15 +93,13 @@ namespace osu.Game.Overlays.Settings
public bool MatchingFilter public bool MatchingFilter
{ {
set => this.FadeTo(value ? 1 : 0); set => Alpha = value ? 1 : 0;
} }
public bool FilteringActive { get; set; } public bool FilteringActive { get; set; }
public event Action SettingChanged; public event Action SettingChanged;
private readonly RestoreDefaultValueButton<T> restoreDefaultButton;
protected SettingsItem() protected SettingsItem()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -110,7 +108,6 @@ namespace osu.Game.Overlays.Settings
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
restoreDefaultButton = new RestoreDefaultValueButton<T>(),
FlowContent = new FillFlowContainer FlowContent = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -123,7 +120,7 @@ namespace osu.Game.Overlays.Settings
}, },
}; };
// all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is // IMPORTANT: all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is
// never loaded, but requires bindable storage. // never loaded, but requires bindable storage.
if (controlWithCurrent == null) if (controlWithCurrent == null)
throw new ArgumentException(@$"Control created via {nameof(CreateControl)} must implement {nameof(IHasCurrentValue<T>)}"); throw new ArgumentException(@$"Control created via {nameof(CreateControl)} must implement {nameof(IHasCurrentValue<T>)}");
@ -132,12 +129,17 @@ namespace osu.Game.Overlays.Settings
controlWithCurrent.Current.DisabledChanged += _ => updateDisabled(); controlWithCurrent.Current.DisabledChanged += _ => updateDisabled();
} }
protected override void LoadComplete() [BackgroundDependencyLoader]
private void load()
{ {
base.LoadComplete(); // intentionally done before LoadComplete to avoid overhead.
if (ShowsDefaultIndicator) if (ShowsDefaultIndicator)
restoreDefaultButton.Current = controlWithCurrent.Current; {
AddInternal(new RestoreDefaultValueButton<T>
{
Current = controlWithCurrent.Current,
});
}
} }
private void updateDisabled() private void updateDisabled()

View File

@ -22,6 +22,9 @@ namespace osu.Game.Overlays.Settings
private readonly Box selectionIndicator; private readonly Box selectionIndicator;
private readonly Container text; private readonly Container text;
// always consider as part of flow, even when not visible (for the sake of the initial animation).
public override bool IsPresent => true;
private SettingsSection section; private SettingsSection section;
public SettingsSection Section public SettingsSection Section

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -58,6 +59,12 @@ namespace osu.Game.Overlays
private readonly bool showSidebar; private readonly bool showSidebar;
private LoadingLayer loading;
private readonly List<SettingsSection> loadableSections = new List<SettingsSection>();
private Task sectionsLoadingTask;
protected SettingsPanel(bool showSidebar) protected SettingsPanel(bool showSidebar)
{ {
this.showSidebar = showSidebar; this.showSidebar = showSidebar;
@ -86,69 +93,48 @@ namespace osu.Game.Overlays
Colour = OsuColour.Gray(0.05f), Colour = OsuColour.Gray(0.05f),
Alpha = 1, Alpha = 1,
}, },
SectionsContainer = new SettingsSectionsContainer loading = new LoadingLayer
{ {
Masking = true, State = { Value = Visibility.Visible }
RelativeSizeAxes = Axes.Both, }
ExpandableHeader = CreateHeader(),
FixedHeader = searchTextBox = new SeekLimitedSearchTextBox
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Width = 0.95f,
Margin = new MarginPadding
{
Top = 20,
Bottom = 20
},
},
Footer = CreateFooter()
},
} }
}; };
Add(SectionsContainer = new SettingsSectionsContainer
{
Masking = true,
RelativeSizeAxes = Axes.Both,
ExpandableHeader = CreateHeader(),
FixedHeader = searchTextBox = new SeekLimitedSearchTextBox
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Width = 0.95f,
Margin = new MarginPadding
{
Top = 20,
Bottom = 20
},
},
Footer = CreateFooter().With(f => f.Alpha = 0)
});
if (showSidebar) if (showSidebar)
{ {
AddInternal(Sidebar = new Sidebar { Width = sidebar_width }); AddInternal(Sidebar = new Sidebar { Width = sidebar_width });
SectionsContainer.SelectedSection.ValueChanged += section =>
{
selectedSidebarButton.Selected = false;
selectedSidebarButton = Sidebar.Children.Single(b => b.Section == section.NewValue);
selectedSidebarButton.Selected = true;
};
} }
searchTextBox.Current.ValueChanged += term => SectionsContainer.SearchContainer.SearchTerm = term.NewValue;
CreateSections()?.ForEach(AddSection); CreateSections()?.ForEach(AddSection);
} }
protected void AddSection(SettingsSection section) protected void AddSection(SettingsSection section)
{ {
SectionsContainer.Add(section); if (IsLoaded)
// just to keep things simple. can be accommodated for if we ever need it.
throw new InvalidOperationException("All sections must be added before the panel is loaded.");
if (Sidebar != null) loadableSections.Add(section);
{
var button = new SidebarButton
{
Section = section,
Action = () =>
{
SectionsContainer.ScrollTo(section);
Sidebar.State = ExpandedState.Contracted;
},
};
Sidebar.Add(button);
if (selectedSidebarButton == null)
{
selectedSidebarButton = Sidebar.Children.First();
selectedSidebarButton.Selected = true;
}
}
} }
protected virtual Drawable CreateHeader() => new Container(); protected virtual Drawable CreateHeader() => new Container();
@ -161,6 +147,12 @@ namespace osu.Game.Overlays
ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint); ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint);
// delay load enough to ensure it doesn't overlap with the initial animation.
// this is done as there is still a brief stutter during load completion which is more visible if the transition is in progress.
// the eventual goal would be to remove the need for this by splitting up load into smaller work pieces, or fixing the remaining
// load complete overheads.
Scheduler.AddDelayed(loadSections, TRANSITION_LENGTH / 3);
Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint);
this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint);
@ -199,6 +191,78 @@ namespace osu.Game.Overlays
Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 }; Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 };
} }
private const double fade_in_duration = 1000;
private void loadSections()
{
if (sectionsLoadingTask != null)
return;
sectionsLoadingTask = LoadComponentsAsync(loadableSections, sections =>
{
SectionsContainer.AddRange(sections);
SectionsContainer.Footer.FadeInFromZero(fade_in_duration, Easing.OutQuint);
SectionsContainer.SearchContainer.FadeInFromZero(fade_in_duration, Easing.OutQuint);
loading.Hide();
searchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchContainer.SearchTerm = term.NewValue, true);
searchTextBox.TakeFocus();
loadSidebarButtons();
});
}
private void loadSidebarButtons()
{
if (Sidebar == null)
return;
LoadComponentsAsync(createSidebarButtons(), buttons =>
{
float delay = 0;
foreach (var button in buttons)
{
Sidebar.Add(button);
button.FadeOut()
.Delay(delay)
.FadeInFromZero(fade_in_duration, Easing.OutQuint);
delay += 40;
}
SectionsContainer.SelectedSection.BindValueChanged(section =>
{
if (selectedSidebarButton != null)
selectedSidebarButton.Selected = false;
selectedSidebarButton = Sidebar.Children.Single(b => b.Section == section.NewValue);
selectedSidebarButton.Selected = true;
}, true);
});
}
private IEnumerable<SidebarButton> createSidebarButtons()
{
foreach (var section in SectionsContainer)
{
yield return new SidebarButton
{
Section = section,
Action = () =>
{
if (!SectionsContainer.IsLoaded)
return;
SectionsContainer.ScrollTo(section);
Sidebar.State = ExpandedState.Contracted;
},
};
}
}
private class NonMaskedContent : Container<Drawable> private class NonMaskedContent : Container<Drawable>
{ {
// masking breaks the pan-out transform with nested sub-settings panels. // masking breaks the pan-out transform with nested sub-settings panels.

View File

@ -129,6 +129,17 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore] [JsonIgnore]
public virtual Type[] IncompatibleMods => Array.Empty<Type>(); public virtual Type[] IncompatibleMods => Array.Empty<Type>();
private IReadOnlyList<IBindable> settingsBacking;
/// <summary>
/// A list of the all <see cref="IBindable"/> settings within this mod.
/// </summary>
internal IReadOnlyList<IBindable> Settings =>
settingsBacking ??= this.GetSettingsSourceProperties()
.Select(p => p.Item2.GetValue(this))
.Cast<IBindable>()
.ToList();
/// <summary> /// <summary>
/// Creates a copy of this <see cref="Mod"/> initialised to a default state. /// Creates a copy of this <see cref="Mod"/> initialised to a default state.
/// </summary> /// </summary>
@ -191,15 +202,39 @@ namespace osu.Game.Rulesets.Mods
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;
return GetType() == other.GetType() && return GetType() == other.GetType() &&
this.GetSettingsSourceProperties().All(pair => Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
EqualityComparer<object>.Default.Equals( }
ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(this)),
ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(other)))); public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(GetType());
foreach (var setting in Settings)
hashCode.Add(ModUtils.GetSettingUnderlyingValue(setting));
return hashCode.ToHashCode();
} }
/// <summary> /// <summary>
/// Reset all custom settings for this mod back to their defaults. /// Reset all custom settings for this mod back to their defaults.
/// </summary> /// </summary>
public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType())); public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType()));
private class ModSettingsEqualityComparer : IEqualityComparer<IBindable>
{
public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer();
public bool Equals(IBindable x, IBindable y)
{
object xValue = x == null ? null : ModUtils.GetSettingUnderlyingValue(x);
object yValue = y == null ? null : ModUtils.GetSettingUnderlyingValue(y);
return EqualityComparer<object>.Default.Equals(xValue, yValue);
}
public int GetHashCode(IBindable obj) => ModUtils.GetSettingUnderlyingValue(obj).GetHashCode();
}
} }
} }

View File

@ -150,8 +150,6 @@ namespace osu.Game.Screens.Edit
if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First()) if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First())
seekTime = timingPoint.Time; seekTime = timingPoint.Time;
// Ensure the sought point is within the boundaries
seekTime = Math.Clamp(seekTime, 0, TrackLength);
SeekSmoothlyTo(seekTime); SeekSmoothlyTo(seekTime);
} }
@ -190,6 +188,9 @@ namespace osu.Game.Screens.Edit
seekingOrStopped.Value = IsSeeking = true; seekingOrStopped.Value = IsSeeking = true;
ClearTransforms(); ClearTransforms();
// Ensure the sought point is within the boundaries
position = Math.Clamp(position, 0, TrackLength);
return underlyingClock.Seek(position); return underlyingClock.Seek(position);
} }
@ -288,7 +289,7 @@ namespace osu.Game.Screens.Edit
} }
private void transformSeekTo(double seek, double duration = 0, Easing easing = Easing.None) private void transformSeekTo(double seek, double duration = 0, Easing easing = Easing.None)
=> this.TransformTo(this.PopulateTransform(new TransformSeek(), seek, duration, easing)); => this.TransformTo(this.PopulateTransform(new TransformSeek(), Math.Clamp(seek, 0, TrackLength), duration, easing));
private double currentTime private double currentTime
{ {

View File

@ -10,8 +10,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Screens.Ranking.Expanded;
using osuTK; using osuTK;
namespace osu.Game.Screens.OnlinePlay.Components namespace osu.Game.Screens.OnlinePlay.Components
@ -64,8 +64,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
minDisplay = new StarRatingDisplay(default), minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range),
maxDisplay = new StarRatingDisplay(default) maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range)
} }
} }
}; };

View File

@ -78,6 +78,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load([CanBeNull] IdleTracker idleTracker) private void load([CanBeNull] IdleTracker idleTracker)
{ {
const float controls_area_height = 25f;
if (idleTracker != null) if (idleTracker != null)
isIdle.BindTo(idleTracker.IsIdle); isIdle.BindTo(idleTracker.IsIdle);
@ -86,87 +88,74 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
ListingPollingComponent = CreatePollingComponent().With(c => c.Filter.BindTarget = filter), ListingPollingComponent = CreatePollingComponent().With(c => c.Filter.BindTarget = filter),
loadingLayer = new LoadingLayer(true),
new Container new Container
{ {
Name = @"Rooms area",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding Padding = new MarginPadding
{ {
Left = WaveOverlayContainer.WIDTH_PADDING, Horizontal = WaveOverlayContainer.WIDTH_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20,
}, },
Child = new GridContainer Child = scrollContainer = new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RowDimensions = new[] ScrollbarOverlapsContent = false,
Child = roomsContainer = new RoomsContainer
{ {
new Dimension(GridSizeMode.Absolute, Header.HEIGHT), Filter = { BindTarget = filter },
new Dimension(GridSizeMode.Absolute, 25), SelectedRoom = { BindTarget = selectedRoom }
new Dimension(GridSizeMode.Absolute, 20) }
},
},
loadingLayer = new LoadingLayer(true),
new FillFlowContainer
{
Name = @"Header area flow",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
Height = Header.HEIGHT,
Child = searchTextBox = new LoungeSearchTextBox
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Width = 0.6f,
},
}, },
Content = new[] new Container
{ {
new Drawable[] RelativeSizeAxes = Axes.X,
Height = controls_area_height,
Children = new Drawable[]
{ {
searchTextBox = new LoungeSearchTextBox Buttons.WithChild(CreateNewRoomButton().With(d =>
{ {
Anchor = Anchor.CentreRight, d.Anchor = Anchor.BottomLeft;
Origin = Anchor.CentreRight, d.Origin = Anchor.BottomLeft;
RelativeSizeAxes = Axes.X, d.Size = new Vector2(150, 37.5f);
Width = 0.6f, d.Action = () => Open();
}, })),
}, new FillFlowContainer
new Drawable[]
{
new Container
{ {
RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopRight,
Depth = float.MinValue, // Contained filters should appear over the top of rooms. Origin = Anchor.TopRight,
Children = new Drawable[] AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10),
ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d =>
{ {
Buttons.WithChild(CreateNewRoomButton().With(d => d.Anchor = Anchor.TopRight;
{ d.Origin = Anchor.TopRight;
d.Anchor = Anchor.BottomLeft; }))
d.Origin = Anchor.BottomLeft;
d.Size = new Vector2(150, 37.5f);
d.Action = () => Open();
})),
new FillFlowContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10),
ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d =>
{
d.Anchor = Anchor.TopRight;
d.Origin = Anchor.TopRight;
}))
}
}
} }
},
null,
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
scrollContainer = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Child = roomsContainer = new RoomsContainer
{
Filter = { BindTarget = filter },
SelectedRoom = { BindTarget = selectedRoom }
}
},
}
},
} }
} }
}, },

View File

@ -19,9 +19,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private ChannelManager channelManager { get; set; } private ChannelManager channelManager { get; set; }
public MatchChatDisplay() private readonly bool leaveChannelOnDispose;
public MatchChatDisplay(bool leaveChannelOnDispose = true)
: base(true) : base(true)
{ {
this.leaveChannelOnDispose = leaveChannelOnDispose;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -42,7 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
channelManager?.LeaveChannel(Channel.Value);
if (leaveChannelOnDispose)
channelManager?.LeaveChannel(Channel.Value);
} }
} }
} }

View File

@ -195,14 +195,19 @@ namespace osu.Game.Screens.OnlinePlay.Match
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new[] Children = new Drawable[]
{ {
new Box new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"28242d") // Temporary. Colour = Color4Extensions.FromHex(@"28242d") // Temporary.
}, },
CreateFooter() new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(5),
Child = CreateFooter()
},
} }
} }
} }
@ -252,6 +257,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (Room.RoomID.Value == null) if (Room.RoomID.Value == null)
{ {
// room has not been created yet; exit immediately. // room has not been created yet; exit immediately.
settingsOverlay.Hide();
return base.OnBackButton(); return base.OnBackButton();
} }

View File

@ -0,0 +1,111 @@
// 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.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class GameplayChatDisplay : MatchChatDisplay, IKeyBindingHandler<GlobalAction>
{
[Resolved]
private ILocalUserPlayInfo localUserInfo { get; set; }
private IBindable<bool> localUserPlaying = new Bindable<bool>();
public override bool PropagatePositionalInputSubTree => !localUserPlaying.Value;
public Bindable<bool> Expanded = new Bindable<bool>();
private readonly Bindable<bool> expandedFromTextboxFocus = new Bindable<bool>();
private const float height = 100;
public override bool PropagateNonPositionalInputSubTree => true;
public GameplayChatDisplay()
: base(leaveChannelOnDispose: false)
{
RelativeSizeAxes = Axes.X;
Background.Alpha = 0.2f;
Textbox.FocusLost = () => expandedFromTextboxFocus.Value = false;
}
protected override void LoadComplete()
{
base.LoadComplete();
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(playing =>
{
// for now let's never hold focus. this avoid misdirected gameplay keys entering chat.
// note that this is done within this callback as it triggers an un-focus as well.
Textbox.HoldFocus = false;
// only hold focus (after sending a message) during breaks
Textbox.ReleaseFocusOnCommit = playing.NewValue;
}, true);
Expanded.BindValueChanged(_ => updateExpandedState(), true);
expandedFromTextboxFocus.BindValueChanged(focus =>
{
if (focus.NewValue)
updateExpandedState();
else
{
// on finishing typing a message there should be a brief delay before hiding.
using (BeginDelayedSequence(600))
updateExpandedState();
}
}, true);
}
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.ToggleChatFocus:
if (Textbox.HasFocus)
{
Schedule(() => Textbox.KillFocus());
}
else
{
expandedFromTextboxFocus.Value = true;
// schedule required to ensure the textbox has become present from above bindable update.
Schedule(() => Textbox.TakeFocus());
}
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
private void updateExpandedState()
{
if (Expanded.Value || expandedFromTextboxFocus.Value)
{
this.FadeIn(300, Easing.OutQuint);
this.ResizeHeightTo(height, 500, Easing.OutQuint);
}
else
{
this.FadeOut(300, Easing.OutQuint);
this.ResizeHeightTo(0, 500, Easing.OutQuint);
}
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
@ -35,6 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new Drawable[] Children = new Drawable[]
{ {
beatmapPanelContainer = new Container beatmapPanelContainer = new Container

View File

@ -53,7 +53,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
new Dimension(), new Dimension(),
new Dimension(maxSize: spectate_button_width), new Dimension(maxSize: spectate_button_width),
new Dimension(GridSizeMode.Absolute, 10), new Dimension(GridSizeMode.Absolute, 5),
new Dimension(maxSize: ready_button_width), new Dimension(maxSize: ready_button_width),
new Dimension() new Dimension()
} }

View File

@ -158,6 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
LengthLimit = 100,
}, },
}, },
new Section("Room visibility") new Section("Room visibility")
@ -215,6 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
LengthLimit = 255,
}, },
}, },
} }

View File

@ -130,79 +130,68 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// Spacer // Spacer
null, null,
// Main right column // Main right column
new FillFlowContainer new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both,
AutoSizeAxes = Axes.Y, Content = new[]
Children = new[]
{ {
new FillFlowContainer new Drawable[] { new OverlinedHeader("Beatmap") },
new Drawable[] { new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } },
new[]
{ {
RelativeSizeAxes = Axes.X, UserModsSection = new FillFlowContainer
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{ {
new OverlinedHeader("Beatmap"), RelativeSizeAxes = Axes.X,
new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } AutoSizeAxes = Axes.Y,
} Margin = new MarginPadding { Top = 10 },
}, Alpha = 0,
UserModsSection = new FillFlowContainer Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, new OverlinedHeader("Extra mods"),
Direction = FillDirection.Horizontal, new FillFlowContainer
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{ {
new UserModSelectButton AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{ {
Anchor = Anchor.CentreLeft, new UserModSelectButton
Origin = Anchor.CentreLeft, {
Width = 90, Anchor = Anchor.CentreLeft,
Text = "Select", Origin = Anchor.CentreLeft,
Action = ShowUserModSelect, Width = 90,
}, Text = "Select",
new ModDisplay Action = ShowUserModSelect,
{ },
Anchor = Anchor.CentreLeft, new ModDisplay
Origin = Anchor.CentreLeft, {
Current = UserMods, Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.8f), Origin = Anchor.CentreLeft,
}, Current = UserMods,
} Scale = new Vector2(0.8f),
},
}
},
} }
} },
} },
new Drawable[] { new OverlinedHeader("Chat") { Margin = new MarginPadding { Vertical = 5 }, }, },
new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } }
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
} }
} },
} }
} }
} }
} }
}, },
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[] { new OverlinedHeader("Chat") },
new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } }
}
}
}
}, },
}; };

View File

@ -68,6 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(5)
}); });
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
@ -78,7 +79,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
((IBindable<bool>)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud); ((IBindable<bool>)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud);
leaderboardFlow.Add(l); leaderboardFlow.Insert(0, l);
if (leaderboard.TeamScores.Count >= 2) if (leaderboard.TeamScores.Count >= 2)
{ {
@ -87,10 +88,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value }, Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value }, Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
Expanded = { BindTarget = HUDOverlay.ShowHud }, Expanded = { BindTarget = HUDOverlay.ShowHud },
}, leaderboardFlow.Add); }, scoreDisplay => leaderboardFlow.Insert(1, scoreDisplay));
} }
}); });
LoadComponentAsync(new GameplayChatDisplay
{
Expanded = { BindTarget = HUDOverlay.ShowHud },
}, chat => leaderboardFlow.Insert(2, chat));
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
} }

View File

@ -23,12 +23,6 @@ namespace osu.Game.Screens.OnlinePlay
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
public const float X_SHIFT = 200;
public const double X_MOVE_DURATION = 800;
public const double RESUME_TRANSITION_DELAY = DISAPPEAR_DURATION / 2;
public const double APPEAR_DURATION = 800; public const double APPEAR_DURATION = 800;
public const double DISAPPEAR_DURATION = 500; public const double DISAPPEAR_DURATION = 500;
@ -36,9 +30,7 @@ namespace osu.Game.Screens.OnlinePlay
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
this.FadeInFromZero(APPEAR_DURATION, Easing.OutQuint); this.FadeInFromZero(APPEAR_DURATION, Easing.OutQuint);
this.MoveToX(X_SHIFT).MoveToX(0, X_MOVE_DURATION, Easing.OutQuint);
} }
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
@ -46,7 +38,6 @@ namespace osu.Game.Screens.OnlinePlay
base.OnExiting(next); base.OnExiting(next);
this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint); this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint);
this.MoveToX(X_SHIFT, X_MOVE_DURATION, Easing.OutQuint);
return false; return false;
} }
@ -54,9 +45,7 @@ namespace osu.Game.Screens.OnlinePlay
public override void OnResuming(IScreen last) public override void OnResuming(IScreen last)
{ {
base.OnResuming(last); base.OnResuming(last);
this.FadeIn(APPEAR_DURATION, Easing.OutQuint);
this.Delay(RESUME_TRANSITION_DELAY).FadeIn(APPEAR_DURATION, Easing.OutQuint);
this.MoveToX(0, X_MOVE_DURATION, Easing.OutQuint);
} }
public override void OnSuspending(IScreen next) public override void OnSuspending(IScreen next)
@ -64,7 +53,6 @@ namespace osu.Game.Screens.OnlinePlay
base.OnSuspending(next); base.OnSuspending(next);
this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint); this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint);
this.MoveToX(-X_SHIFT, X_MOVE_DURATION, Easing.OutQuint);
} }
public override string ToString() => Title; public override string ToString() => Title;

View File

@ -120,6 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 }, Margin = new MarginPadding { Bottom = 10 },
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -10,12 +10,12 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking.Expanded;
using osuTK; using osuTK;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play

View File

@ -75,7 +75,9 @@ namespace osu.Game.Screens.Play
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>(); private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
protected readonly Bindable<bool> LocalUserPlaying = new Bindable<bool>(); public IBindable<bool> LocalUserPlaying => localUserPlaying;
private readonly Bindable<bool> localUserPlaying = new Bindable<bool>();
public int RestartCount; public int RestartCount;
@ -442,7 +444,7 @@ namespace osu.Game.Screens.Play
{ {
bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value; bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value;
OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered;
LocalUserPlaying.Value = inGameplay; localUserPlaying.Value = inGameplay;
} }
private void updateSampleDisabledState() private void updateSampleDisabledState()

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;

View File

@ -1,129 +0,0 @@
// 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.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Ranking.Expanded
{
/// <summary>
/// A pill that displays the star rating of a <see cref="BeatmapInfo"/>.
/// </summary>
public class StarRatingDisplay : CompositeDrawable, IHasCurrentValue<StarDifficulty>
{
private Box background;
private FillFlowContainer content;
private OsuTextFlowContainer textFlow;
[Resolved]
private OsuColour colours { get; set; }
private readonly BindableWithCurrent<StarDifficulty> current = new BindableWithCurrent<StarDifficulty>();
public Bindable<StarDifficulty> Current
{
get => current.Current;
set => current.Current = value;
}
/// <summary>
/// Creates a new <see cref="StarRatingDisplay"/> using an already computed <see cref="StarDifficulty"/>.
/// </summary>
/// <param name="starDifficulty">The already computed <see cref="StarDifficulty"/> to display the star difficulty of.</param>
public StarRatingDisplay(StarDifficulty starDifficulty)
{
Current.Value = starDifficulty;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache)
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
}
},
content = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 8, Vertical = 4 },
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2, 0),
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(7),
Icon = FontAwesome.Solid.Star,
},
textFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.Numeric.With(weight: FontWeight.Black))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
TextAnchor = Anchor.BottomLeft,
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => updateDisplay(), true);
}
private void updateDisplay()
{
var starRatingParts = Current.Value.Stars.ToString("0.00", CultureInfo.InvariantCulture).Split('.');
string wholePart = starRatingParts[0];
string fractionPart = starRatingParts[1];
string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
var stars = Current.Value.Stars;
background.Colour = colours.ForStarDifficulty(stars);
content.Colour = stars >= 6.5 ? colours.Orange1 : Color4.Black;
textFlow.Clear();
textFlow.AddText($"{wholePart}", s =>
{
s.Font = s.Font.With(size: 14);
s.UseFullGlyphHeight = false;
});
textFlow.AddText($"{separator}{fractionPart}", s =>
{
s.Font = s.Font.With(size: 7);
s.UseFullGlyphHeight = false;
});
}
}
}

View File

@ -28,7 +28,6 @@ using osu.Game.Extensions;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
@ -38,6 +37,8 @@ namespace osu.Game.Screens.Select
public const float BORDER_THICKNESS = 2.5f; public const float BORDER_THICKNESS = 2.5f;
private const float shear_width = 36.75f; private const float shear_width = 36.75f;
private const float transition_duration = 250;
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
[Resolved] [Resolved]
@ -46,11 +47,6 @@ namespace osu.Game.Screens.Select
[Resolved] [Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } private IBindable<IReadOnlyList<Mod>> mods { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
private IBindable<StarDifficulty?> beatmapDifficulty;
protected Container DisplayedContent { get; private set; } protected Container DisplayedContent { get; private set; }
protected WedgeInfoText Info { get; private set; } protected WedgeInfoText Info { get; private set; }
@ -77,24 +73,24 @@ namespace osu.Game.Screens.Select
ruleset.BindValueChanged(_ => updateDisplay()); ruleset.BindValueChanged(_ => updateDisplay());
} }
private const double animation_duration = 800;
protected override void PopIn() protected override void PopIn()
{ {
this.MoveToX(0, 800, Easing.OutQuint); this.MoveToX(0, animation_duration, Easing.OutQuint);
this.RotateTo(0, 800, Easing.OutQuint); this.RotateTo(0, animation_duration, Easing.OutQuint);
this.FadeIn(250); this.FadeIn(transition_duration);
} }
protected override void PopOut() protected override void PopOut()
{ {
this.MoveToX(-100, 800, Easing.In); this.MoveToX(-100, animation_duration, Easing.In);
this.RotateTo(10, 800, Easing.In); this.RotateTo(10, animation_duration, Easing.In);
this.FadeOut(500, Easing.In); this.FadeOut(transition_duration * 2, Easing.In);
} }
private WorkingBeatmap beatmap; private WorkingBeatmap beatmap;
private CancellationTokenSource cancellationSource;
public WorkingBeatmap Beatmap public WorkingBeatmap Beatmap
{ {
get => beatmap; get => beatmap;
@ -103,12 +99,6 @@ namespace osu.Game.Screens.Select
if (beatmap == value) return; if (beatmap == value) return;
beatmap = value; beatmap = value;
cancellationSource?.Cancel();
cancellationSource = new CancellationTokenSource();
beatmapDifficulty?.UnbindAll();
beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token);
beatmapDifficulty.BindValueChanged(_ => updateDisplay());
updateDisplay(); updateDisplay();
} }
@ -128,7 +118,7 @@ namespace osu.Game.Screens.Select
{ {
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
DisplayedContent?.FadeOut(250); DisplayedContent?.FadeOut(transition_duration);
DisplayedContent?.Expire(); DisplayedContent?.Expire();
DisplayedContent = null; DisplayedContent = null;
} }
@ -147,7 +137,7 @@ namespace osu.Game.Screens.Select
Children = new Drawable[] Children = new Drawable[]
{ {
new BeatmapInfoWedgeBackground(beatmap), new BeatmapInfoWedgeBackground(beatmap),
Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty()), Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value),
} }
}, loaded => }, loaded =>
{ {
@ -160,12 +150,6 @@ namespace osu.Game.Screens.Select
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
cancellationSource?.Cancel();
}
public class WedgeInfoText : Container public class WedgeInfoText : Container
{ {
public OsuSpriteText VersionLabel { get; private set; } public OsuSpriteText VersionLabel { get; private set; }
@ -174,6 +158,9 @@ namespace osu.Game.Screens.Select
public BeatmapSetOnlineStatusPill StatusPill { get; private set; } public BeatmapSetOnlineStatusPill StatusPill { get; private set; }
public FillFlowContainer MapperContainer { get; private set; } public FillFlowContainer MapperContainer { get; private set; }
private Container difficultyColourBar;
private StarRatingDisplay starRatingDisplay;
private ILocalisedBindableString titleBinding; private ILocalisedBindableString titleBinding;
private ILocalisedBindableString artistBinding; private ILocalisedBindableString artistBinding;
private FillFlowContainer infoLabelContainer; private FillFlowContainer infoLabelContainer;
@ -182,20 +169,21 @@ namespace osu.Game.Screens.Select
private readonly WorkingBeatmap beatmap; private readonly WorkingBeatmap beatmap;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
private readonly StarDifficulty starDifficulty;
private ModSettingChangeTracker settingChangeTracker; private ModSettingChangeTracker settingChangeTracker;
public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty) public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
this.mods = mods; this.mods = mods;
starDifficulty = difficulty;
} }
private CancellationTokenSource cancellationSource;
private IBindable<StarDifficulty?> starDifficulty;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LocalisationManager localisation) private void load(OsuColour colours, LocalisationManager localisation, BeatmapDifficultyCache difficultyCache)
{ {
var beatmapInfo = beatmap.BeatmapInfo; var beatmapInfo = beatmap.BeatmapInfo;
var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
@ -205,12 +193,30 @@ namespace osu.Game.Screens.Select
titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist));
const float top_height = 0.7f;
Children = new Drawable[] Children = new Drawable[]
{ {
new DifficultyColourBar(starDifficulty) difficultyColourBar = new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = 20, Width = 20f,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Width = top_height,
},
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Alpha = 0.5f,
X = top_height,
Width = 1 - top_height,
}
}
}, },
new FillFlowContainer new FillFlowContainer
{ {
@ -241,14 +247,16 @@ namespace osu.Game.Screens.Select
Padding = new MarginPadding { Top = 14, Right = shear_width / 2 }, Padding = new MarginPadding { Top = 14, Right = shear_width / 2 },
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Shear = wedged_container_shear, Shear = wedged_container_shear,
Children = new[] Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{ {
createStarRatingDisplay(starDifficulty).With(display => starRatingDisplay = new StarRatingDisplay(default, animated: true)
{ {
display.Anchor = Anchor.TopRight; Anchor = Anchor.TopRight,
display.Origin = Anchor.TopRight; Origin = Anchor.TopRight,
display.Shear = -wedged_container_shear; Shear = -wedged_container_shear,
}), Alpha = 0f,
},
StatusPill = new BeatmapSetOnlineStatusPill StatusPill = new BeatmapSetOnlineStatusPill
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
@ -304,6 +312,18 @@ namespace osu.Game.Screens.Select
titleBinding.BindValueChanged(_ => setMetadata(metadata.Source)); titleBinding.BindValueChanged(_ => setMetadata(metadata.Source));
artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true); artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true);
starRatingDisplay.DisplayedStars.BindValueChanged(s =>
{
difficultyColourBar.Colour = colours.ForStarDifficulty(s.NewValue);
}, true);
starDifficulty = difficultyCache.GetBindableDifficulty(beatmapInfo, (cancellationSource = new CancellationTokenSource()).Token);
starDifficulty.BindValueChanged(s =>
{
starRatingDisplay.FadeIn(transition_duration);
starRatingDisplay.Current.Value = s.NewValue ?? default;
});
// no difficulty means it can't have a status to show // no difficulty means it can't have a status to show
if (beatmapInfo.Version == null) if (beatmapInfo.Version == null)
StatusPill.Hide(); StatusPill.Hide();
@ -311,13 +331,6 @@ namespace osu.Game.Screens.Select
addInfoLabels(); addInfoLabels();
} }
private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0
? new StarRatingDisplay(difficulty)
{
Margin = new MarginPadding { Bottom = 5 }
}
: Empty();
private void setMetadata(string source) private void setMetadata(string source)
{ {
ArtistLabel.Text = artistBinding.Value; ArtistLabel.Text = artistBinding.Value;
@ -429,6 +442,7 @@ namespace osu.Game.Screens.Select
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
settingChangeTracker?.Dispose(); settingChangeTracker?.Dispose();
cancellationSource?.Cancel();
} }
public class InfoLabel : Container, IHasTooltip public class InfoLabel : Container, IHasTooltip
@ -489,43 +503,6 @@ namespace osu.Game.Screens.Select
}; };
} }
} }
private class DifficultyColourBar : Container
{
private readonly StarDifficulty difficulty;
public DifficultyColourBar(StarDifficulty difficulty)
{
this.difficulty = difficulty;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
const float full_opacity_ratio = 0.7f;
var difficultyColour = colours.ForStarDifficulty(difficulty.Stars);
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = difficultyColour,
Width = full_opacity_ratio,
},
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Colour = difficultyColour,
Alpha = 0.5f,
X = full_opacity_ratio,
Width = 1 - full_opacity_ratio,
}
};
}
}
} }
} }
} }

View File

@ -1,150 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
/// <summary> /// <summary>
/// A <see cref="RoomManager"/> for use in multiplayer test scenes. Should generally not be used by itself outside of a <see cref="MultiplayerTestScene"/>. /// A <see cref="RoomManager"/> for use in multiplayer test scenes, backed by a <see cref="TestRoomRequestsHandler"/>.
/// Should generally not be used by itself outside of a <see cref="MultiplayerTestScene"/>.
/// </summary> /// </summary>
/// <remarks>
/// This implementation will pretend to be a server, handling room retrieval and manipulation requests
/// and returning a roughly expected state, without the need for a server to be running.
/// </remarks>
public class TestRequestHandlingMultiplayerRoomManager : MultiplayerRoomManager public class TestRequestHandlingMultiplayerRoomManager : MultiplayerRoomManager
{ {
[Resolved] public IReadOnlyList<Room> ServerSideRooms => handler.ServerSideRooms;
private IAPIProvider api { get; set; }
[Resolved] private readonly TestRoomRequestsHandler handler = new TestRoomRequestsHandler();
private OsuGameBase game { get; set; }
public IReadOnlyList<Room> ServerSideRooms => serverSideRooms;
private readonly List<Room> serverSideRooms = new List<Room>();
private int currentRoomId;
private int currentPlaylistItemId;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(IAPIProvider api, OsuGameBase game)
{ {
int currentScoreId = 0; ((DummyAPIAccess)api).HandleRequest = request => handler.HandleRequest(request, api.LocalUser.Value, game);
// Handling here is pretending to be a server, while also updating the local state to match
// how the server would eventually respond and update the RoomManager.
((DummyAPIAccess)api).HandleRequest = req =>
{
switch (req)
{
case CreateRoomRequest createRoomRequest:
var apiRoom = new Room();
apiRoom.CopyFrom(createRoomRequest.Room);
// Passwords are explicitly not copied between rooms.
apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value);
apiRoom.Password.Value = createRoomRequest.Room.Password.Value;
AddServerSideRoom(apiRoom);
var responseRoom = new APICreatedRoom();
responseRoom.CopyFrom(createResponseRoom(apiRoom, false));
createRoomRequest.TriggerSuccess(responseRoom);
return true;
case JoinRoomRequest joinRoomRequest:
{
var room = ServerSideRooms.Single(r => r.RoomID.Value == joinRoomRequest.Room.RoomID.Value);
if (joinRoomRequest.Password != room.Password.Value)
{
joinRoomRequest.TriggerFailure(new InvalidOperationException("Invalid password."));
return true;
}
joinRoomRequest.TriggerSuccess();
return true;
}
case PartRoomRequest partRoomRequest:
partRoomRequest.TriggerSuccess();
return true;
case GetRoomsRequest getRoomsRequest:
var roomsWithoutParticipants = new List<Room>();
foreach (var r in ServerSideRooms)
roomsWithoutParticipants.Add(createResponseRoom(r, false));
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
return true;
case GetRoomRequest getRoomRequest:
getRoomRequest.TriggerSuccess(createResponseRoom(ServerSideRooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId), true));
return true;
case GetBeatmapSetRequest getBeatmapSetRequest:
var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type);
onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res);
onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e);
// Get the online API from the game's dependencies.
game.Dependencies.Get<IAPIProvider>().Queue(onlineReq);
return true;
case CreateRoomScoreRequest createRoomScoreRequest:
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
return true;
case SubmitRoomScoreRequest submitRoomScoreRequest:
submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = 1,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = ScoreRank.S,
MaxCombo = 1000,
TotalScore = 1000000,
User = api.LocalUser.Value,
Statistics = new Dictionary<HitResult, int>()
});
return true;
}
return false;
};
} }
public void AddServerSideRoom(Room room) /// <summary>
{ /// Adds a room to a local "server-side" list that's returned when a <see cref="GetRoomsRequest"/> is fired.
room.RoomID.Value ??= currentRoomId++; /// </summary>
for (int i = 0; i < room.Playlist.Count; i++) /// <param name="room">The room.</param>
room.Playlist[i].ID = currentPlaylistItemId++; public void AddServerSideRoom(Room room) => handler.AddServerSideRoom(room);
serverSideRooms.Add(room);
}
private Room createResponseRoom(Room room, bool withParticipants)
{
var responseRoom = new Room();
responseRoom.CopyFrom(room);
responseRoom.Password.Value = null;
if (!withParticipants)
responseRoom.RecentParticipants.Clear();
return responseRoom;
}
} }
} }

View File

@ -1,111 +0,0 @@
// 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 System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.OnlinePlay
{
/// <summary>
/// A very simple <see cref="RoomManager"/> for use in online play test scenes.
/// </summary>
public class BasicTestRoomManager : IRoomManager
{
public event Action RoomsUpdated;
public readonly BindableList<Room> Rooms = new BindableList<Room>();
public Action<Room, string> JoinRoomRequested;
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
IBindableList<Room> IRoomManager.Rooms => Rooms;
private int currentRoomId;
public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
room.RoomID.Value ??= Rooms.Select(r => r.RoomID.Value).Where(id => id != null).Select(id => id.Value).DefaultIfEmpty().Max() + 1;
onSuccess?.Invoke(room);
AddOrUpdateRoom(room);
}
public void AddOrUpdateRoom(Room room)
{
var existing = Rooms.FirstOrDefault(r => r.RoomID.Value != null && r.RoomID.Value == room.RoomID.Value);
if (existing != null)
existing.CopyFrom(room);
else
Rooms.Add(room);
RoomsUpdated?.Invoke();
}
public void RemoveRoom(Room room)
{
Rooms.Remove(room);
RoomsUpdated?.Invoke();
}
public void ClearRooms()
{
Rooms.Clear();
RoomsUpdated?.Invoke();
}
public void JoinRoom(Room room, string password, Action<Room> onSuccess = null, Action<string> onError = null)
{
JoinRoomRequested?.Invoke(room, password);
onSuccess?.Invoke(room);
}
public void PartRoom()
{
}
public void AddRooms(int count, RulesetInfo ruleset = null, bool withPassword = false)
{
for (int i = 0; i < count; i++)
{
var room = new Room
{
RoomID = { Value = currentRoomId },
Position = { Value = currentRoomId },
Name = { Value = $"Room {currentRoomId}" },
Host = { Value = new User { Username = "Host" } },
EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) },
Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal },
Password = { Value = withPassword ? "password" : string.Empty }
};
if (ruleset != null)
{
room.Playlist.Add(new PlaylistItem
{
Ruleset = { Value = ruleset },
Beatmap =
{
Value = new BeatmapInfo
{
Metadata = new BeatmapMetadata()
}
}
});
}
CreateRoom(room);
currentRoomId++;
}
}
}
}

View File

@ -71,6 +71,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
drawableComponents.Add(drawable); drawableComponents.Add(drawable);
} }
protected virtual IRoomManager CreateRoomManager() => new BasicTestRoomManager(); protected virtual IRoomManager CreateRoomManager() => new TestRequestHandlingRoomManager();
} }
} }

View File

@ -0,0 +1,75 @@
// 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 osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.OnlinePlay
{
/// <summary>
/// A very simple <see cref="RoomManager"/> for use in online play test scenes.
/// </summary>
public class TestRequestHandlingRoomManager : RoomManager
{
public Action<Room, string> JoinRoomRequested;
private int currentRoomId;
private readonly TestRoomRequestsHandler handler = new TestRoomRequestsHandler();
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuGameBase game)
{
((DummyAPIAccess)api).HandleRequest = request => handler.HandleRequest(request, api.LocalUser.Value, game);
}
public override void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{
JoinRoomRequested?.Invoke(room, password);
base.JoinRoom(room, password, onSuccess, onError);
}
public void AddRooms(int count, RulesetInfo ruleset = null, bool withPassword = false)
{
for (int i = 0; i < count; i++)
{
var room = new Room
{
RoomID = { Value = -currentRoomId },
Name = { Value = $@"Room {currentRoomId}" },
Host = { Value = new User { Username = @"Host" } },
EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) },
Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal },
};
if (withPassword)
room.Password.Value = @"password";
if (ruleset != null)
{
room.Playlist.Add(new PlaylistItem
{
Ruleset = { Value = ruleset },
Beatmap =
{
Value = new BeatmapInfo
{
Metadata = new BeatmapMetadata()
}
}
});
}
CreateRoom(room);
currentRoomId++;
}
}
}
}

View File

@ -0,0 +1,147 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.OnlinePlay
{
/// <summary>
/// Represents a handler which pretends to be a server, handling room retrieval and manipulation requests
/// and returning a roughly expected state, without the need for a server to be running.
/// </summary>
public class TestRoomRequestsHandler
{
public IReadOnlyList<Room> ServerSideRooms => serverSideRooms;
private readonly List<Room> serverSideRooms = new List<Room>();
private int currentRoomId;
private int currentPlaylistItemId;
private int currentScoreId;
/// <summary>
/// Handles an API request, while also updating the local state to match
/// how the server would eventually respond and update an <see cref="RoomManager"/>.
/// </summary>
/// <param name="request">The API request to handle.</param>
/// <param name="localUser">The local user to store in responses where required.</param>
/// <param name="game">The game base for cases where actual online requests need to be sent.</param>
/// <returns>Whether the request was successfully handled.</returns>
public bool HandleRequest(APIRequest request, User localUser, OsuGameBase game)
{
switch (request)
{
case CreateRoomRequest createRoomRequest:
var apiRoom = new Room();
apiRoom.CopyFrom(createRoomRequest.Room);
// Passwords are explicitly not copied between rooms.
apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value);
apiRoom.Password.Value = createRoomRequest.Room.Password.Value;
AddServerSideRoom(apiRoom);
var responseRoom = new APICreatedRoom();
responseRoom.CopyFrom(createResponseRoom(apiRoom, false));
createRoomRequest.TriggerSuccess(responseRoom);
return true;
case JoinRoomRequest joinRoomRequest:
{
var room = ServerSideRooms.Single(r => r.RoomID.Value == joinRoomRequest.Room.RoomID.Value);
if (joinRoomRequest.Password != room.Password.Value)
{
joinRoomRequest.TriggerFailure(new InvalidOperationException("Invalid password."));
return true;
}
joinRoomRequest.TriggerSuccess();
return true;
}
case PartRoomRequest partRoomRequest:
partRoomRequest.TriggerSuccess();
return true;
case GetRoomsRequest getRoomsRequest:
var roomsWithoutParticipants = new List<Room>();
foreach (var r in ServerSideRooms)
roomsWithoutParticipants.Add(createResponseRoom(r, false));
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
return true;
case GetRoomRequest getRoomRequest:
getRoomRequest.TriggerSuccess(createResponseRoom(ServerSideRooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId), true));
return true;
case GetBeatmapSetRequest getBeatmapSetRequest:
var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type);
onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res);
onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e);
// Get the online API from the game's dependencies.
game.Dependencies.Get<IAPIProvider>().Queue(onlineReq);
return true;
case CreateRoomScoreRequest createRoomScoreRequest:
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
return true;
case SubmitRoomScoreRequest submitRoomScoreRequest:
submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = 1,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = ScoreRank.S,
MaxCombo = 1000,
TotalScore = 1000000,
User = localUser,
Statistics = new Dictionary<HitResult, int>()
});
return true;
}
return false;
}
/// <summary>
/// Adds a room to a local "server-side" list that's returned when a <see cref="GetRoomsRequest"/> is fired.
/// </summary>
/// <param name="room">The room.</param>
public void AddServerSideRoom(Room room)
{
room.RoomID.Value ??= currentRoomId++;
for (int i = 0; i < room.Playlist.Count; i++)
room.Playlist[i].ID = currentPlaylistItemId++;
serverSideRooms.Add(room);
}
private Room createResponseRoom(Room room, bool withParticipants)
{
var responseRoom = new Room();
responseRoom.CopyFrom(room);
responseRoom.Password.Value = null;
if (!withParticipants)
responseRoom.RecentParticipants.Clear();
return responseRoom;
}
}
}

View File

@ -105,8 +105,9 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeProtected_002EGlobal/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeProtected_002EGlobal/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeCastWithTypeCheck/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeCastWithTypeCheck/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeConditionalExpression/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeConditionalExpression/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeIntoNegatedPattern/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeIntoPattern/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeSequentialChecks/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeSequentialChecks/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeSequentialPatterns/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodHasAsyncOverload/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodHasAsyncOverload/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodHasAsyncOverloadWithCancellation/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodHasAsyncOverloadWithCancellation/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodSupportsCancellation/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodSupportsCancellation/@EntryIndexedValue">WARNING</s:String>