Merge branch 'master' into remove-current-room

This commit is contained in:
smoogipoo
2021-08-20 16:23:36 +09:00
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", () =>
{
settings?.Expire();
Add(settings = new SettingsOverlay
{ {
State = { Value = Visibility.Visible } State = { Value = Visibility.Visible }
};
Add(dialogOverlay = new DialogOverlay
{
Depth = -1
}); });
});
}
[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,10 +95,12 @@ 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
protected override void UpdateSize(Vector2 newSize) protected override void UpdateSize(Vector2 newSize)

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();
if (IsLoaded)
UpdateState(); 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[]
{
dropdown = new AudioDeviceSettingsDropdown
{
Keywords = new[] { "speaker", "headphone", "output" }
}
};
if (audio != null) updateItems();
{
audio.OnNewDevice -= onDeviceChanged; audio.OnNewDevice += onDeviceChanged;
audio.OnLostDevice -= 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)
{
base.Dispose(isDisposing);
protected override void LoadComplete() if (audio != null)
{ {
base.LoadComplete(); audio.OnNewDevice -= onDeviceChanged;
audio.OnLostDevice -= onDeviceChanged;
Children = new Drawable[]
{
dropdown = new AudioDeviceSettingsDropdown
{
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,7 +93,14 @@ namespace osu.Game.Overlays
Colour = OsuColour.Gray(0.05f), Colour = OsuColour.Gray(0.05f),
Alpha = 1, Alpha = 1,
}, },
SectionsContainer = new SettingsSectionsContainer loading = new LoadingLayer
{
State = { Value = Visibility.Visible }
}
}
};
Add(SectionsContainer = new SettingsSectionsContainer
{ {
Masking = true, Masking = true,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -103,52 +117,24 @@ namespace osu.Game.Overlays
Bottom = 20 Bottom = 20
}, },
}, },
Footer = CreateFooter() 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,29 +88,41 @@ 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) }
}, },
Content = new[] },
loadingLayer = new LoadingLayer(true),
new FillFlowContainer
{ {
new Drawable[] Name = @"Header area flow",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
searchTextBox = new LoungeSearchTextBox new Container
{
RelativeSizeAxes = Axes.X,
Height = Header.HEIGHT,
Child = searchTextBox = new LoungeSearchTextBox
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
@ -116,12 +130,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
Width = 0.6f, Width = 0.6f,
}, },
}, },
new Drawable[]
{
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.X,
Depth = float.MinValue, // Contained filters should appear over the top of rooms. Height = controls_area_height,
Children = new Drawable[] Children = new Drawable[]
{ {
Buttons.WithChild(CreateNewRoomButton().With(d => Buttons.WithChild(CreateNewRoomButton().With(d =>
@ -147,29 +159,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
} }
} }
}, },
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,6 +45,8 @@ 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);
if (leaveChannelOnDispose)
channelManager?.LeaveChannel(Channel.Value); 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,27 +130,21 @@ 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,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new OverlinedHeader("Beatmap"),
new BeatmapSelectionControl { RelativeSizeAxes = Axes.X }
}
},
UserModsSection = new FillFlowContainer UserModsSection = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 }, Margin = new MarginPadding { Top = 10 },
Alpha = 0,
Children = new Drawable[] Children = new Drawable[]
{ {
new OverlinedHeader("Extra mods"), new OverlinedHeader("Extra mods"),
@ -177,32 +171,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Scale = new Vector2(0.8f), Scale = new Vector2(0.8f),
}, },
} }
} },
}
}
}
}
}
}
}
} }
}, },
new Drawable[] },
{ new Drawable[] { new OverlinedHeader("Chat") { Margin = new MarginPadding { Vertical = 5 }, }, },
new GridContainer new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } }
{ },
RelativeSizeAxes = Axes.Both,
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize) new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
}, },
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(); /// <summary>
return true; /// 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>
case PartRoomRequest partRoomRequest: public void AddServerSideRoom(Room room) => handler.AddServerSideRoom(room);
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)
{
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

@ -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>