1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 23:12:56 +08:00

Merge branch 'master' into fix-leave-room-race-2

This commit is contained in:
Bartłomiej Dach 2021-01-30 13:41:04 +01:00
commit 9ab1ad25eb
100 changed files with 1448 additions and 587 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.118.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.128.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -24,16 +24,13 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageReference Include="System.IO.Packaging" Version="5.0.0" /> <PackageReference Include="System.IO.Packaging" Version="5.0.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" /> <PackageReference Include="ppy.squirrel.windows" Version="1.9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.0.169" /> <PackageReference Include="DiscordRichPresence" Version="1.0.169" />
<!-- .NET 3.1 SDK seems to cause issues with a runtime specification. This will likely be resolved in .NET 5. -->
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
<PackageReference Include="System.Runtime.Handles" Version="4.3.0" />
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />

View File

@ -45,6 +45,11 @@ namespace osu.Game.Rulesets.Catch.Replays
float positionChange = Math.Abs(lastPosition - h.EffectiveX); float positionChange = Math.Abs(lastPosition - h.EffectiveX);
double timeAvailable = h.StartTime - lastTime; double timeAvailable = h.StartTime - lastTime;
if (timeAvailable < 0)
{
return;
}
// So we can either make it there without a dash or not. // So we can either make it there without a dash or not.
// If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too) // If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too)
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour. // The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.

View File

@ -1,11 +1,45 @@
// 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.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModDifficultyAdjust : ModDifficultyAdjust public class TaikoModDifficultyAdjust : ModDifficultyAdjust
{ {
[SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ScrollSpeed { get; } = new BindableFloat
{
Precision = 0.05f,
MinValue = 0.25f,
MaxValue = 4,
Default = 1,
Value = 1,
};
public override string SettingDescription
{
get
{
string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N1}";
return string.Join(", ", new[]
{
base.SettingDescription,
scrollSpeed
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
ApplySetting(ScrollSpeed, scroll => difficulty.SliderMultiplier *= scroll);
}
} }
} }

View File

@ -1,6 +1,7 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
@ -8,5 +9,16 @@ namespace osu.Game.Rulesets.Taiko.Mods
public class TaikoModEasy : ModEasy public class TaikoModEasy : ModEasy
{ {
public override string Description => @"Beats move slower, and less accuracy required!"; public override string Description => @"Beats move slower, and less accuracy required!";
/// <summary>
/// Multiplier factor added to the scrolling speed.
/// </summary>
private const double slider_multiplier = 0.8;
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.SliderMultiplier *= slider_multiplier;
}
} }
} }

View File

@ -1,6 +1,7 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
@ -9,5 +10,21 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => 1.06;
public override bool Ranked => true; public override bool Ranked => true;
/// <summary>
/// Multiplier factor added to the scrolling speed.
/// </summary>
/// <remarks>
/// This factor is made up of two parts: the base part (1.4) and the aspect ratio adjustment (4/3).
/// Stable applies the latter by dividing the width of the user's display by the width of a display with the same height, but 4:3 aspect ratio.
/// TODO: Revisit if taiko playfield ever changes away from a hard-coded 16:9 (see https://github.com/ppy/osu/issues/5685).
/// </remarks>
private const double slider_multiplier = 1.4 * 4 / 3;
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.SliderMultiplier *= slider_multiplier;
}
} }
} }

View File

@ -0,0 +1,62 @@
// 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 NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.NonVisual
{
[HeadlessTest]
public class OngoingOperationTrackerTest : OsuTestScene
{
private OngoingOperationTracker tracker;
private IBindable<bool> operationInProgress;
[SetUpSteps]
public void SetUp()
{
AddStep("create tracker", () => Child = tracker = new OngoingOperationTracker());
AddStep("bind to operation status", () => operationInProgress = tracker.InProgress.GetBoundCopy());
}
[Test]
public void TestOperationTracking()
{
IDisposable firstOperation = null;
IDisposable secondOperation = null;
AddStep("begin first operation", () => firstOperation = tracker.BeginOperation());
AddAssert("first operation in progress", () => operationInProgress.Value);
AddStep("cannot start another operation",
() => Assert.Throws<InvalidOperationException>(() => tracker.BeginOperation()));
AddStep("end first operation", () => firstOperation.Dispose());
AddAssert("first operation is ended", () => !operationInProgress.Value);
AddStep("start second operation", () => secondOperation = tracker.BeginOperation());
AddAssert("second operation in progress", () => operationInProgress.Value);
AddStep("dispose first operation again", () => firstOperation.Dispose());
AddAssert("second operation still in progress", () => operationInProgress.Value);
AddStep("dispose second operation", () => secondOperation.Dispose());
AddAssert("second operation is ended", () => !operationInProgress.Value);
}
[Test]
public void TestOperationDisposalAfterTracker()
{
IDisposable operation = null;
AddStep("begin operation", () => operation = tracker.BeginOperation());
AddStep("dispose tracker", () => tracker.Expire());
AddStep("end operation", () => operation.Dispose());
AddAssert("operation is ended", () => !operationInProgress.Value);
}
}
}

View File

@ -5,6 +5,8 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK; using osuTK;
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture] [TestFixture]
public class TestSceneEditorSummaryTimeline : EditorClockTestScene public class TestSceneEditorSummaryTimeline : EditorClockTestScene
{ {
[Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {

View File

@ -17,10 +17,12 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -129,6 +131,31 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("no DHOs shown", () => !this.ChildrenOfType<DrawableTestHitObject>().Any()); AddUntilStep("no DHOs shown", () => !this.ChildrenOfType<DrawableTestHitObject>().Any());
} }
[Test]
public void TestApplyHitResultOnKilled()
{
ManualClock clock = null;
bool anyJudged = false;
void onNewResult(JudgementResult _) => anyJudged = true;
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new TestKilledHitObject { Duration = 20 });
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
AddStep("subscribe to new result", () =>
{
anyJudged = false;
drawableRuleset.NewResult += onNewResult;
});
AddStep("skip past object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() + 1000);
AddAssert("object judged", () => anyJudged);
AddStep("clean up", () => drawableRuleset.NewResult -= onNewResult);
}
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null) => AddStep("create test", () => private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null) => AddStep("create test", () =>
{ {
var ruleset = new TestPoolingRuleset(); var ruleset = new TestPoolingRuleset();
@ -192,6 +219,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void load() private void load()
{ {
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize); RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize);
} }
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
@ -220,19 +248,30 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{ {
switch (original)
{
case TestKilledHitObject h:
yield return h;
break;
default:
yield return new TestHitObject yield return new TestHitObject
{ {
StartTime = original.StartTime, StartTime = original.StartTime,
Duration = 250 Duration = 250
}; };
break;
}
} }
} }
#endregion #endregion
#region HitObject #region HitObjects
private class TestHitObject : ConvertHitObject private class TestHitObject : ConvertHitObject, IHasDuration
{ {
public double EndTime => StartTime + Duration; public double EndTime => StartTime + Duration;
@ -287,6 +326,30 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
} }
private class TestKilledHitObject : TestHitObject
{
}
private class DrawableTestKilledHitObject : DrawableHitObject<TestKilledHitObject>
{
public DrawableTestKilledHitObject()
: base(null)
{
}
protected override void UpdateHitStateTransforms(ArmedState state)
{
base.UpdateHitStateTransforms(state);
Expire();
}
public override void OnKilled()
{
base.OnKilled();
ApplyResult(r => r.Type = r.Judgement.MinResult);
}
}
#endregion #endregion
} }
} }

View File

@ -1,32 +1,81 @@
// 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 osu.Game.Overlays; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
public class TestSceneBeatmapListingOverlay : OsuTestScene public class TestSceneBeatmapListingOverlay : OsuTestScene
{ {
protected override bool UseOnlineAPI => true; private readonly List<APIBeatmapSet> setsForResponse = new List<APIBeatmapSet>();
private readonly BeatmapListingOverlay overlay; private BeatmapListingOverlay overlay;
public TestSceneBeatmapListingOverlay() [BackgroundDependencyLoader]
private void load()
{ {
Add(overlay = new BeatmapListingOverlay()); Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } };
((DummyAPIAccess)API).HandleRequest = req =>
{
if (req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)
{
searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
{
BeatmapSets = setsForResponse,
});
}
};
} }
[Test] [Test]
public void TestShow() public void TestNoBeatmapsPlaceholder()
{ {
AddStep("Show", overlay.Show); AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any());
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
// fetch once more to ensure nothing happens in displaying placeholder again when it already is present.
AddStep("fetch for 0 beatmaps again", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
} }
[Test] private void fetchFor(params BeatmapSetInfo[] beatmaps)
public void TestHide()
{ {
AddStep("Hide", overlay.Hide); setsForResponse.Clear();
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
// trigger arbitrary change for fetching.
overlay.ChildrenOfType<BeatmapListingSearchControl>().Single().Query.TriggerChange();
}
private class TestAPIBeatmapSet : APIBeatmapSet
{
private readonly BeatmapSetInfo beatmapSet;
public TestAPIBeatmapSet(BeatmapSetInfo beatmapSet)
{
this.beatmapSet = beatmapSet;
}
public override BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) => beatmapSet;
} }
} }
} }

View File

@ -231,8 +231,8 @@ namespace osu.Game.Tests.Visual.Online
}); });
}); });
AddAssert("shown beatmaps of current ruleset", () => overlay.Header.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value)));
AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
} }
[Test] [Test]
@ -310,12 +310,12 @@ namespace osu.Game.Tests.Visual.Online
private void downloadAssert(bool shown) private void downloadAssert(bool shown)
{ {
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.DownloadButtonsVisible == shown); AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown);
} }
private class TestBeatmapSetOverlay : BeatmapSetOverlay private class TestBeatmapSetOverlay : BeatmapSetOverlay
{ {
public new Header Header => base.Header; public new BeatmapSetHeader Header => base.Header;
} }
} }
} }

View File

@ -0,0 +1,33 @@
// 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.Game.Overlays;
using NUnit.Framework;
namespace osu.Game.Tests.Visual.Online
{
[Description("uses online API")]
public class TestSceneOnlineBeatmapListingOverlay : OsuTestScene
{
protected override bool UseOnlineAPI => true;
private readonly BeatmapListingOverlay overlay;
public TestSceneOnlineBeatmapListingOverlay()
{
Add(overlay = new BeatmapListingOverlay());
}
[Test]
public void TestShow()
{
AddStep("Show", overlay.Show);
}
[Test]
public void TestHide()
{
AddStep("Hide", overlay.Hide);
}
}
}

View File

@ -7,6 +7,8 @@ using osu.Game.Overlays.Comments;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
@ -16,13 +18,33 @@ namespace osu.Game.Tests.Visual.Online
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private VotePill votePill; [Cached]
private LoginOverlay login;
private TestPill votePill;
private readonly Container pillContainer;
public TestSceneVotePill()
{
AddRange(new Drawable[]
{
pillContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both
},
login = new LoginOverlay()
});
}
[Test] [Test]
public void TestUserCommentPill() public void TestUserCommentPill()
{ {
AddStep("Hide login overlay", () => login.Hide());
AddStep("Log in", logIn); AddStep("Log in", logIn);
AddStep("User comment", () => addVotePill(getUserComment())); AddStep("User comment", () => addVotePill(getUserComment()));
AddAssert("Background is transparent", () => votePill.Background.Alpha == 0);
AddStep("Click", () => votePill.Click()); AddStep("Click", () => votePill.Click());
AddAssert("Not loading", () => !votePill.IsLoading); AddAssert("Not loading", () => !votePill.IsLoading);
} }
@ -30,8 +52,10 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestRandomCommentPill() public void TestRandomCommentPill()
{ {
AddStep("Hide login overlay", () => login.Hide());
AddStep("Log in", logIn); AddStep("Log in", logIn);
AddStep("Random comment", () => addVotePill(getRandomComment())); AddStep("Random comment", () => addVotePill(getRandomComment()));
AddAssert("Background is visible", () => votePill.Background.Alpha == 1);
AddStep("Click", () => votePill.Click()); AddStep("Click", () => votePill.Click());
AddAssert("Loading", () => votePill.IsLoading); AddAssert("Loading", () => votePill.IsLoading);
} }
@ -39,10 +63,11 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestOfflineRandomCommentPill() public void TestOfflineRandomCommentPill()
{ {
AddStep("Hide login overlay", () => login.Hide());
AddStep("Log out", API.Logout); AddStep("Log out", API.Logout);
AddStep("Random comment", () => addVotePill(getRandomComment())); AddStep("Random comment", () => addVotePill(getRandomComment()));
AddStep("Click", () => votePill.Click()); AddStep("Click", () => votePill.Click());
AddAssert("Not loading", () => !votePill.IsLoading); AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible);
} }
private void logIn() => API.Login("localUser", "password"); private void logIn() => API.Login("localUser", "password");
@ -63,12 +88,22 @@ namespace osu.Game.Tests.Visual.Online
private void addVotePill(Comment comment) private void addVotePill(Comment comment)
{ {
Clear(); pillContainer.Clear();
Add(votePill = new VotePill(comment) pillContainer.Child = votePill = new TestPill(comment)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}); };
}
private class TestPill : VotePill
{
public new Box Background => base.Background;
public TestPill(Comment comment)
: base(comment)
{
}
} }
} }
} }

View File

@ -11,12 +11,10 @@ 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.Graphics.UserInterface;
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;
using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Users; using osu.Game.Users;
@ -85,8 +83,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("move mouse to create button", () => AddStep("move mouse to create button", () =>
{ {
var footer = match.ChildrenOfType<Footer>().Single(); InputManager.MoveMouseTo(this.ChildrenOfType<PlaylistsMatchSettingsOverlay.CreateRoomButton>().Single());
InputManager.MoveMouseTo(footer.ChildrenOfType<OsuButton>().Single());
}); });
AddStep("click", () => InputManager.Click(MouseButton.Left)); AddStep("click", () => InputManager.Click(MouseButton.Left));

View File

@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
SelectedMods.Value = Array.Empty<Mod>();
Children = new Drawable[] Children = new Drawable[]
{ {
modSelect = new TestModSelectOverlay modSelect = new TestModSelectOverlay
@ -134,6 +135,8 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestExternallySetCustomizedMod() public void TestExternallySetCustomizedMod()
{ {
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("ensure button is selected and customized accordingly", () => AddAssert("ensure button is selected and customized accordingly", () =>

View File

@ -0,0 +1,122 @@
// 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.Colour;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneSectionsContainer : OsuManualInputManagerTestScene
{
private readonly SectionsContainer<TestSection> container;
private float custom;
private const float header_height = 100;
public TestSceneSectionsContainer()
{
container = new SectionsContainer<TestSection>
{
RelativeSizeAxes = Axes.Y,
Width = 300,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
FixedHeader = new Box
{
Alpha = 0.5f,
Width = 300,
Height = header_height,
Colour = Color4.Red
}
};
container.SelectedSection.ValueChanged += section =>
{
if (section.OldValue != null)
section.OldValue.Selected = false;
if (section.NewValue != null)
section.NewValue.Selected = true;
};
Add(container);
}
[Test]
public void TestSelection()
{
AddStep("clear", () => container.Clear());
AddStep("add 1/8th", () => append(1 / 8.0f));
AddStep("add third", () => append(1 / 3.0f));
AddStep("add half", () => append(1 / 2.0f));
AddStep("add full", () => append(1));
AddSliderStep("set custom", 0.1f, 1.1f, 0.5f, i => custom = i);
AddStep("add custom", () => append(custom));
AddStep("scroll to previous", () => container.ScrollTo(
container.Children.Reverse().SkipWhile(s => s != container.SelectedSection.Value).Skip(1).FirstOrDefault() ?? container.Children.First()
));
AddStep("scroll to next", () => container.ScrollTo(
container.Children.SkipWhile(s => s != container.SelectedSection.Value).Skip(1).FirstOrDefault() ?? container.Children.Last()
));
AddStep("scroll up", () => triggerUserScroll(1));
AddStep("scroll down", () => triggerUserScroll(-1));
}
[Test]
public void TestCorrectSectionSelected()
{
const int sections_count = 11;
float[] alternating = { 0.07f, 0.33f, 0.16f, 0.33f };
AddStep("clear", () => container.Clear());
AddStep("fill with sections", () =>
{
for (int i = 0; i < sections_count; i++)
append(alternating[i % alternating.Length]);
});
void step(int scrollIndex)
{
AddStep($"scroll to section {scrollIndex + 1}", () => container.ScrollTo(container.Children[scrollIndex]));
AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[scrollIndex]);
}
for (int i = 1; i < sections_count; i++)
step(i);
for (int i = sections_count - 2; i >= 0; i--)
step(i);
AddStep("scroll almost to end", () => container.ScrollTo(container.Children[sections_count - 2]));
AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[sections_count - 2]);
AddStep("scroll down", () => triggerUserScroll(-1));
AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[sections_count - 1]);
}
private static readonly ColourInfo selected_colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Gold);
private static readonly ColourInfo default_colour = ColourInfo.GradientVertical(Color4.White, Color4.DarkGray);
private void append(float multiplier)
{
container.Add(new TestSection
{
Width = 300,
Height = (container.ChildSize.Y - header_height) * multiplier,
Colour = default_colour
});
}
private void triggerUserScroll(float direction)
{
InputManager.MoveMouseTo(container);
InputManager.ScrollVerticalBy(direction);
}
private class TestSection : Box
{
public bool Selected
{
set => Colour = value ? selected_colour : default_colour;
}
}
}
}

View File

@ -0,0 +1,60 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Tournament.Components;
namespace osu.Game.Tournament.Tests.Components
{
public class TestSceneTournamentModDisplay : TournamentTestScene
{
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
private FillFlowContainer<TournamentBeatmapPanel> fillFlow;
private BeatmapInfo beatmap;
[BackgroundDependencyLoader]
private void load()
{
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = 490154 });
req.Success += success;
api.Queue(req);
Add(fillFlow = new FillFlowContainer<TournamentBeatmapPanel>
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Full,
Spacing = new osuTK.Vector2(10)
});
}
private void success(APIBeatmap apiBeatmap)
{
beatmap = apiBeatmap.ToBeatmap(rulesets);
var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods();
foreach (var mod in mods)
{
fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
}
}
}

View File

@ -1,6 +1,8 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
@ -16,5 +18,23 @@ namespace osu.Game.Tournament.Tests.Screens
Add(new TourneyVideo("main") { RelativeSizeAxes = Axes.Both }); Add(new TourneyVideo("main") { RelativeSizeAxes = Axes.Both });
Add(new ScheduleScreen()); Add(new ScheduleScreen());
} }
[Test]
public void TestCurrentMatchTime()
{
setMatchDate(TimeSpan.FromDays(-1));
setMatchDate(TimeSpan.FromSeconds(5));
setMatchDate(TimeSpan.FromMinutes(4));
setMatchDate(TimeSpan.FromHours(3));
}
private void setMatchDate(TimeSpan relativeTime)
// Humanizer cannot handle negative timespans.
=> AddStep($"start time is {relativeTime}", () =>
{
var match = CreateSampleMatch();
match.Date.Value = DateTimeOffset.Now + relativeTime;
Ladder.CurrentMatch.Value = match;
});
} }
} }

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; 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.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -23,7 +22,7 @@ namespace osu.Game.Tournament.Components
public class TournamentBeatmapPanel : CompositeDrawable public class TournamentBeatmapPanel : CompositeDrawable
{ {
public readonly BeatmapInfo Beatmap; public readonly BeatmapInfo Beatmap;
private readonly string mods; private readonly string mod;
private const float horizontal_padding = 10; private const float horizontal_padding = 10;
private const float vertical_padding = 10; private const float vertical_padding = 10;
@ -33,12 +32,12 @@ namespace osu.Game.Tournament.Components
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>(); private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
private Box flash; private Box flash;
public TournamentBeatmapPanel(BeatmapInfo beatmap, string mods = null) public TournamentBeatmapPanel(BeatmapInfo beatmap, string mod = null)
{ {
if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
Beatmap = beatmap; Beatmap = beatmap;
this.mods = mods; this.mod = mod;
Width = 400; Width = 400;
Height = HEIGHT; Height = HEIGHT;
} }
@ -122,23 +121,15 @@ namespace osu.Game.Tournament.Components
}, },
}); });
if (!string.IsNullOrEmpty(mods)) if (!string.IsNullOrEmpty(mod))
{ {
AddInternal(new Container AddInternal(new TournamentModIcon(mod)
{ {
RelativeSizeAxes = Axes.Y,
Width = 60,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Margin = new MarginPadding(10), Margin = new MarginPadding(10),
Child = new Sprite Width = 60,
{ RelativeSizeAxes = Axes.Y,
FillMode = FillMode.Fit,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Texture = textures.Get($"mods/{mods}"),
}
}); });
} }
} }

View File

@ -0,0 +1,65 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets;
using osu.Game.Rulesets.UI;
using osu.Game.Tournament.Models;
using osuTK;
namespace osu.Game.Tournament.Components
{
/// <summary>
/// Mod icon displayed in tournament usages, allowing user overridden graphics.
/// </summary>
public class TournamentModIcon : CompositeDrawable
{
private readonly string modAcronym;
[Resolved]
private RulesetStore rulesets { get; set; }
public TournamentModIcon(string modAcronym)
{
this.modAcronym = modAcronym;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures, LadderInfo ladderInfo)
{
var customTexture = textures.Get($"mods/{modAcronym}");
if (customTexture != null)
{
AddInternal(new Sprite
{
FillMode = FillMode.Fit,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Texture = customTexture
});
return;
}
var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0);
var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == modAcronym);
if (modIcon == null)
return;
AddInternal(new ModIcon(modIcon, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0.5f)
});
}
}
}

View File

@ -192,12 +192,7 @@ namespace osu.Game.Tournament.Screens.Schedule
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Children = new Drawable[] Children = new Drawable[]
{ {
new TournamentSpriteText new ScheduleMatchDate(match.NewValue.Date.Value)
{
Text = "Starting ",
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
},
new DrawableDate(match.NewValue.Date.Value)
{ {
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
} }
@ -251,6 +246,18 @@ namespace osu.Game.Tournament.Screens.Schedule
} }
} }
public class ScheduleMatchDate : DrawableDate
{
public ScheduleMatchDate(DateTimeOffset date, float textSize = OsuFont.DEFAULT_FONT_SIZE, bool italic = true)
: base(date, textSize, italic)
{
}
protected override string Format() => Date < DateTimeOffset.Now
? $"Started {base.Format()}"
: $"Starting {base.Format()}";
}
public class ScheduleContainer : Container public class ScheduleContainer : Container
{ {
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;

View File

@ -50,15 +50,7 @@ namespace osu.Game.Beatmaps
IBeatmap IBeatmap.Clone() => Clone(); IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
{
var clone = (Beatmap<T>)MemberwiseClone();
clone.ControlPointInfo = ControlPointInfo.CreateCopy();
// todo: deep clone other elements as required.
return clone;
}
} }
public class Beatmap : Beatmap<HitObject> public class Beatmap : Beatmap<HitObject>

View File

@ -7,7 +7,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
@ -154,23 +153,34 @@ namespace osu.Game.Beatmaps
{ {
using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online"))) using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online")))
{ {
var found = db.QuerySingleOrDefault<CachedOnlineBeatmapLookup>( db.Open();
"SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap);
if (found != null) using (var cmd = db.CreateCommand())
{ {
var status = (BeatmapSetOnlineStatus)found.approved; cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path";
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash));
cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value));
cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path));
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
var status = (BeatmapSetOnlineStatus)reader.GetByte(2);
beatmap.Status = status; beatmap.Status = status;
beatmap.BeatmapSet.Status = status; beatmap.BeatmapSet.Status = status;
beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id; beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0);
beatmap.OnlineBeatmapID = found.beatmap_id; beatmap.OnlineBeatmapID = reader.GetInt32(1);
LogForModel(set, $"Cached local retrieval for {beatmap}."); LogForModel(set, $"Cached local retrieval for {beatmap}.");
return true; return true;
} }
} }
} }
}
}
catch (Exception ex) catch (Exception ex)
{ {
LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}.");

View File

@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// The control points in this beatmap. /// The control points in this beatmap.
/// </summary> /// </summary>
ControlPointInfo ControlPointInfo { get; } ControlPointInfo ControlPointInfo { get; set; }
/// <summary> /// <summary>
/// The breaks in this beatmap. /// The breaks in this beatmap.

View File

@ -6,34 +6,11 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
namespace osu.Game.Extensions namespace osu.Game.Extensions
{ {
public static class TaskExtensions public static class TaskExtensions
{ {
/// <summary>
/// Denote a task which is to be run without local error handling logic, where failure is not catastrophic.
/// Avoids unobserved exceptions from being fired.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="logAsError">
/// Whether errors should be logged as errors visible to users, or as debug messages.
/// Logging as debug will essentially silence the errors on non-release builds.
/// </param>
public static Task CatchUnobservedExceptions(this Task task, bool logAsError = false)
{
return task.ContinueWith(t =>
{
Exception? exception = t.Exception?.AsSingular();
if (logAsError)
Logger.Error(exception, $"Error running task: {exception?.Message ?? "(unknown)"}", LoggingTarget.Runtime, true);
else
Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug);
}, TaskContinuationOptions.NotOnRanToCompletion);
}
/// <summary> /// <summary>
/// Add a continuation to be performed only after the attached task has completed. /// Add a continuation to be performed only after the attached task has completed.
/// </summary> /// </summary>

View File

@ -24,6 +24,10 @@ namespace osu.Game.Graphics.Containers
private Bindable<bool> parallaxEnabled; private Bindable<bool> parallaxEnabled;
private const float parallax_duration = 100;
private bool firstUpdate = true;
public ParallaxContainer() public ParallaxContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -60,17 +64,27 @@ namespace osu.Game.Graphics.Containers
input = GetContainingInputManager(); input = GetContainingInputManager();
} }
private bool firstUpdate = true;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (parallaxEnabled.Value) if (parallaxEnabled.Value)
{ {
Vector2 offset = (input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.Position) - DrawSize / 2) * ParallaxAmount; Vector2 offset = Vector2.Zero;
const float parallax_duration = 100; if (input.CurrentState.Mouse != null)
{
var sizeDiv2 = DrawSize / 2;
Vector2 relativeAmount = ToLocalSpace(input.CurrentState.Mouse.Position) - sizeDiv2;
const float base_factor = 0.999f;
relativeAmount.X = (float)(Math.Sign(relativeAmount.X) * Interpolation.Damp(0, 1, base_factor, Math.Abs(relativeAmount.X)));
relativeAmount.Y = (float)(Math.Sign(relativeAmount.Y) * Interpolation.Damp(0, 1, base_factor, Math.Abs(relativeAmount.Y)));
offset = relativeAmount * sizeDiv2 * ParallaxAmount;
}
double elapsed = Math.Clamp(Clock.ElapsedFrameTime, 0, parallax_duration); double elapsed = Math.Clamp(Clock.ElapsedFrameTime, 0, parallax_duration);

View File

@ -2,6 +2,7 @@
// 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;
using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -9,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Framework.Utils;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
@ -20,6 +22,7 @@ namespace osu.Game.Graphics.Containers
where T : Drawable where T : Drawable
{ {
public Bindable<T> SelectedSection { get; } = new Bindable<T>(); public Bindable<T> SelectedSection { get; } = new Bindable<T>();
private Drawable lastClickedSection;
public Drawable ExpandableHeader public Drawable ExpandableHeader
{ {
@ -36,7 +39,7 @@ namespace osu.Game.Graphics.Containers
if (value == null) return; if (value == null) return;
AddInternal(expandableHeader); AddInternal(expandableHeader);
lastKnownScroll = float.NaN; lastKnownScroll = null;
} }
} }
@ -52,7 +55,7 @@ namespace osu.Game.Graphics.Containers
if (value == null) return; if (value == null) return;
AddInternal(fixedHeader); AddInternal(fixedHeader);
lastKnownScroll = float.NaN; lastKnownScroll = null;
} }
} }
@ -71,7 +74,7 @@ namespace osu.Game.Graphics.Containers
footer.Anchor |= Anchor.y2; footer.Anchor |= Anchor.y2;
footer.Origin |= Anchor.y2; footer.Origin |= Anchor.y2;
scrollContainer.Add(footer); scrollContainer.Add(footer);
lastKnownScroll = float.NaN; lastKnownScroll = null;
} }
} }
@ -89,21 +92,26 @@ namespace osu.Game.Graphics.Containers
headerBackgroundContainer.Add(headerBackground); headerBackgroundContainer.Add(headerBackground);
lastKnownScroll = float.NaN; lastKnownScroll = null;
} }
} }
protected override Container<T> Content => scrollContentContainer; protected override Container<T> Content => scrollContentContainer;
private readonly OsuScrollContainer scrollContainer; private readonly UserTrackingScrollContainer scrollContainer;
private readonly Container headerBackgroundContainer; private readonly Container headerBackgroundContainer;
private readonly MarginPadding originalSectionsMargin; private readonly MarginPadding originalSectionsMargin;
private Drawable expandableHeader, fixedHeader, footer, headerBackground; private Drawable expandableHeader, fixedHeader, footer, headerBackground;
private FlowContainer<T> scrollContentContainer; private FlowContainer<T> scrollContentContainer;
private float headerHeight, footerHeight; private float? headerHeight, footerHeight;
private float lastKnownScroll; private float? lastKnownScroll;
/// <summary>
/// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section).
/// </summary>
private const float scroll_y_centre = 0.1f;
public SectionsContainer() public SectionsContainer()
{ {
@ -128,18 +136,24 @@ namespace osu.Game.Graphics.Containers
public override void Add(T drawable) public override void Add(T drawable)
{ {
base.Add(drawable); base.Add(drawable);
lastKnownScroll = float.NaN;
headerHeight = float.NaN; Debug.Assert(drawable != null);
footerHeight = float.NaN;
lastKnownScroll = null;
headerHeight = null;
footerHeight = null;
} }
public void ScrollTo(Drawable section) => public void ScrollTo(Drawable section)
scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); {
lastClickedSection = section;
scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - scrollContainer.DisplayableContent * scroll_y_centre - (FixedHeader?.BoundingBox.Height ?? 0));
}
public void ScrollToTop() => scrollContainer.ScrollTo(0); public void ScrollToTop() => scrollContainer.ScrollTo(0);
[NotNull] [NotNull]
protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); protected virtual UserTrackingScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer();
[NotNull] [NotNull]
protected virtual FlowContainer<T> CreateScrollContentContainer() => protected virtual FlowContainer<T> CreateScrollContentContainer() =>
@ -156,7 +170,7 @@ namespace osu.Game.Graphics.Containers
if (source == InvalidationSource.Child && (invalidation & Invalidation.DrawSize) != 0) if (source == InvalidationSource.Child && (invalidation & Invalidation.DrawSize) != 0)
{ {
lastKnownScroll = -1; lastKnownScroll = null;
result = true; result = true;
} }
@ -167,7 +181,10 @@ namespace osu.Game.Graphics.Containers
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
float headerH = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); float fixedHeaderSize = FixedHeader?.LayoutSize.Y ?? 0;
float expandableHeaderSize = ExpandableHeader?.LayoutSize.Y ?? 0;
float headerH = expandableHeaderSize + fixedHeaderSize;
float footerH = Footer?.LayoutSize.Y ?? 0; float footerH = Footer?.LayoutSize.Y ?? 0;
if (headerH != headerHeight || footerH != footerHeight) if (headerH != headerHeight || footerH != footerHeight)
@ -183,28 +200,39 @@ namespace osu.Game.Graphics.Containers
{ {
lastKnownScroll = currentScroll; lastKnownScroll = currentScroll;
// reset last clicked section because user started scrolling themselves
if (scrollContainer.UserScrolling)
lastClickedSection = null;
if (ExpandableHeader != null && FixedHeader != null) if (ExpandableHeader != null && FixedHeader != null)
{ {
float offset = Math.Min(ExpandableHeader.LayoutSize.Y, currentScroll); float offset = Math.Min(expandableHeaderSize, currentScroll);
ExpandableHeader.Y = -offset; ExpandableHeader.Y = -offset;
FixedHeader.Y = -offset + ExpandableHeader.LayoutSize.Y; FixedHeader.Y = -offset + expandableHeaderSize;
} }
headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); headerBackgroundContainer.Height = expandableHeaderSize + fixedHeaderSize;
headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0; headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0;
float scrollOffset = FixedHeader?.LayoutSize.Y ?? 0; var smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0;
Func<T, float> diff = section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset;
if (scrollContainer.IsScrolledToEnd()) // scroll offset is our fixed header height if we have it plus 10% of content height
{ // plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards
SelectedSection.Value = Children.LastOrDefault(); // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly
} float selectionLenienceAboveSection = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f);
float scrollCentre = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_y_centre + selectionLenienceAboveSection;
if (Precision.AlmostBigger(0, scrollContainer.Current))
SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault();
else if (Precision.AlmostBigger(scrollContainer.Current, scrollContainer.ScrollableExtent))
SelectedSection.Value = lastClickedSection as T ?? Children.LastOrDefault();
else else
{ {
SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault() SelectedSection.Value = Children
?? Children.FirstOrDefault(); .TakeWhile(section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollCentre <= 0)
.LastOrDefault() ?? Children.FirstOrDefault();
} }
} }
} }
@ -214,8 +242,9 @@ namespace osu.Game.Graphics.Containers
if (!Children.Any()) return; if (!Children.Any()) return;
var newMargin = originalSectionsMargin; var newMargin = originalSectionsMargin;
newMargin.Top += headerHeight;
newMargin.Bottom += footerHeight; newMargin.Top += (headerHeight ?? 0);
newMargin.Bottom += (footerHeight ?? 0);
scrollContentContainer.Margin = newMargin; scrollContentContainer.Margin = newMargin;
} }

View File

@ -0,0 +1,49 @@
// 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.Graphics;
namespace osu.Game.Graphics.Containers
{
public class UserTrackingScrollContainer : UserTrackingScrollContainer<Drawable>
{
public UserTrackingScrollContainer()
{
}
public UserTrackingScrollContainer(Direction direction)
: base(direction)
{
}
}
public class UserTrackingScrollContainer<T> : OsuScrollContainer<T>
where T : Drawable
{
/// <summary>
/// Whether the last scroll event was user triggered, directly on the scroll container.
/// </summary>
public bool UserScrolling { get; private set; }
public UserTrackingScrollContainer()
{
}
public UserTrackingScrollContainer(Direction direction)
: base(direction)
{
}
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
{
UserScrolling = true;
base.OnUserScroll(value, animated, distanceDecay);
}
public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null)
{
UserScrolling = false;
base.ScrollTo(value, animated, distanceDecay);
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -13,16 +14,20 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Online.API namespace osu.Game.Online.API
{ {
[MessagePackObject]
public class APIMod : IMod public class APIMod : IMod
{ {
[JsonProperty("acronym")] [JsonProperty("acronym")]
[Key(0)]
public string Acronym { get; set; } public string Acronym { get; set; }
[JsonProperty("settings")] [JsonProperty("settings")]
[Key(1)]
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>(); public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
[JsonConstructor] [JsonConstructor]
private APIMod() [SerializationConstructor]
public APIMod()
{ {
} }

View File

@ -81,7 +81,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"beatmaps")] [JsonProperty(@"beatmaps")]
private IEnumerable<APIBeatmap> beatmaps { get; set; } private IEnumerable<APIBeatmap> beatmaps { get; set; }
public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) public virtual BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
{ {
var beatmapSet = new BeatmapSetInfo var beatmapSet = new BeatmapSetInfo
{ {

View File

@ -339,7 +339,7 @@ namespace osu.Game.Online.Chat
} }
/// <summary> /// <summary>
/// Joins a channel if it has not already been joined. /// Joins a channel if it has not already been joined. Must be called from the update thread.
/// </summary> /// </summary>
/// <param name="channel">The channel to join.</param> /// <param name="channel">The channel to join.</param>
/// <returns>The joined channel. Note that this may not match the parameter channel as it is a backed object.</returns> /// <returns>The joined channel. Note that this may not match the parameter channel as it is a backed object.</returns>
@ -399,7 +399,11 @@ namespace osu.Game.Online.Chat
return channel; return channel;
} }
public void LeaveChannel(Channel channel) /// <summary>
/// Leave the specified channel. Can be called from any thread.
/// </summary>
/// <param name="channel">The channel to leave.</param>
public void LeaveChannel(Channel channel) => Schedule(() =>
{ {
if (channel == null) return; if (channel == null) return;
@ -413,7 +417,7 @@ namespace osu.Game.Online.Chat
api.Queue(new LeaveChannelRequest(channel)); api.Queue(new LeaveChannelRequest(channel));
channel.Joined.Value = false; channel.Joined.Value = false;
} }
} });
private long lastMessageId; private long lastMessageId;

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -65,13 +66,19 @@ namespace osu.Game.Online.Multiplayer
if (connection != null) if (connection != null)
return; return;
connection = new HubConnectionBuilder() var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options => .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
if (RuntimeInfo.SupportsJIT)
builder.AddMessagePackProtocol();
else
{ {
options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); // eventually we will precompile resolvers for messagepack, but this isn't working currently
}) // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
.Build(); }
connection = builder.Build();
// this is kind of SILLY // this is kind of SILLY
// https://github.com/dotnet/aspnetcore/issues/15198 // https://github.com/dotnet/aspnetcore/issues/15198

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer namespace osu.Game.Online.Multiplayer
@ -13,35 +14,42 @@ namespace osu.Game.Online.Multiplayer
/// A multiplayer room. /// A multiplayer room.
/// </summary> /// </summary>
[Serializable] [Serializable]
[MessagePackObject]
public class MultiplayerRoom public class MultiplayerRoom
{ {
/// <summary> /// <summary>
/// The ID of the room, used for database persistence. /// The ID of the room, used for database persistence.
/// </summary> /// </summary>
[Key(0)]
public readonly long RoomID; public readonly long RoomID;
/// <summary> /// <summary>
/// The current state of the room (ie. whether it is in progress or otherwise). /// The current state of the room (ie. whether it is in progress or otherwise).
/// </summary> /// </summary>
[Key(1)]
public MultiplayerRoomState State { get; set; } public MultiplayerRoomState State { get; set; }
/// <summary> /// <summary>
/// All currently enforced game settings for this room. /// All currently enforced game settings for this room.
/// </summary> /// </summary>
[Key(2)]
public MultiplayerRoomSettings Settings { get; set; } = new MultiplayerRoomSettings(); public MultiplayerRoomSettings Settings { get; set; } = new MultiplayerRoomSettings();
/// <summary> /// <summary>
/// All users currently in this room. /// All users currently in this room.
/// </summary> /// </summary>
[Key(3)]
public List<MultiplayerRoomUser> Users { get; set; } = new List<MultiplayerRoomUser>(); public List<MultiplayerRoomUser> Users { get; set; } = new List<MultiplayerRoomUser>();
/// <summary> /// <summary>
/// The host of this room, in control of changing room settings. /// The host of this room, in control of changing room settings.
/// </summary> /// </summary>
[Key(4)]
public MultiplayerRoomUser? Host { get; set; } public MultiplayerRoomUser? Host { get; set; }
[JsonConstructor] [JsonConstructor]
public MultiplayerRoom(in long roomId) [SerializationConstructor]
public MultiplayerRoom(long roomId)
{ {
RoomID = roomId; RoomID = roomId;
} }

View File

@ -7,22 +7,29 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using MessagePack;
using osu.Game.Online.API; using osu.Game.Online.API;
namespace osu.Game.Online.Multiplayer namespace osu.Game.Online.Multiplayer
{ {
[Serializable] [Serializable]
[MessagePackObject]
public class MultiplayerRoomSettings : IEquatable<MultiplayerRoomSettings> public class MultiplayerRoomSettings : IEquatable<MultiplayerRoomSettings>
{ {
[Key(0)]
public int BeatmapID { get; set; } public int BeatmapID { get; set; }
[Key(1)]
public int RulesetID { get; set; } public int RulesetID { get; set; }
[Key(2)]
public string BeatmapChecksum { get; set; } = string.Empty; public string BeatmapChecksum { get; set; } = string.Empty;
[Key(3)]
public string Name { get; set; } = "Unnamed room"; public string Name { get; set; } = "Unnamed room";
[NotNull] [NotNull]
[Key(4)]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>(); public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
public bool Equals(MultiplayerRoomSettings other) public bool Equals(MultiplayerRoomSettings other)

View File

@ -4,6 +4,7 @@
#nullable enable #nullable enable
using System; using System;
using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Users; using osu.Game.Users;
@ -11,21 +12,26 @@ using osu.Game.Users;
namespace osu.Game.Online.Multiplayer namespace osu.Game.Online.Multiplayer
{ {
[Serializable] [Serializable]
[MessagePackObject]
public class MultiplayerRoomUser : IEquatable<MultiplayerRoomUser> public class MultiplayerRoomUser : IEquatable<MultiplayerRoomUser>
{ {
[Key(0)]
public readonly int UserID; public readonly int UserID;
[Key(1)]
public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle;
/// <summary> /// <summary>
/// The availability state of the current beatmap. /// The availability state of the current beatmap.
/// </summary> /// </summary>
[Key(2)]
public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable();
[IgnoreMember]
public User? User { get; set; } public User? User { get; set; }
[JsonConstructor] [JsonConstructor]
public MultiplayerRoomUser(in int userId) public MultiplayerRoomUser(int userId)
{ {
UserID = userId; UserID = userId;
} }

View File

@ -15,7 +15,6 @@ using osu.Framework.Graphics;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -104,7 +103,7 @@ namespace osu.Game.Online.Multiplayer
if (!connected.NewValue && Room != null) if (!connected.NewValue && Room != null)
{ {
Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important);
LeaveRoom().CatchUnobservedExceptions(); LeaveRoom();
} }
}); });
} }

View File

@ -2,6 +2,7 @@
// 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;
using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Online.Rooms namespace osu.Game.Online.Rooms
@ -9,20 +10,23 @@ namespace osu.Game.Online.Rooms
/// <summary> /// <summary>
/// The local availability information about a certain beatmap for the client. /// The local availability information about a certain beatmap for the client.
/// </summary> /// </summary>
[MessagePackObject]
public class BeatmapAvailability : IEquatable<BeatmapAvailability> public class BeatmapAvailability : IEquatable<BeatmapAvailability>
{ {
/// <summary> /// <summary>
/// The beatmap's availability state. /// The beatmap's availability state.
/// </summary> /// </summary>
[Key(0)]
public readonly DownloadState State; public readonly DownloadState State;
/// <summary> /// <summary>
/// The beatmap's downloading progress, null when not in <see cref="DownloadState.Downloading"/> state. /// The beatmap's downloading progress, null when not in <see cref="DownloadState.Downloading"/> state.
/// </summary> /// </summary>
[Key(1)]
public readonly double? DownloadProgress; public readonly double? DownloadProgress;
[JsonConstructor] [JsonConstructor]
private BeatmapAvailability(DownloadState state, double? downloadProgress = null) public BeatmapAvailability(DownloadState state, double? downloadProgress = null)
{ {
State = state; State = state;
DownloadProgress = downloadProgress; DownloadProgress = downloadProgress;

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -12,10 +13,13 @@ using osu.Game.Scoring;
namespace osu.Game.Online.Spectator namespace osu.Game.Online.Spectator
{ {
[Serializable] [Serializable]
[MessagePackObject]
public class FrameDataBundle public class FrameDataBundle
{ {
[Key(0)]
public FrameHeader Header { get; set; } public FrameHeader Header { get; set; }
[Key(1)]
public IEnumerable<LegacyReplayFrame> Frames { get; set; } public IEnumerable<LegacyReplayFrame> Frames { get; set; }
public FrameDataBundle(ScoreInfo score, IEnumerable<LegacyReplayFrame> frames) public FrameDataBundle(ScoreInfo score, IEnumerable<LegacyReplayFrame> frames)

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -12,31 +13,37 @@ using osu.Game.Scoring;
namespace osu.Game.Online.Spectator namespace osu.Game.Online.Spectator
{ {
[Serializable] [Serializable]
[MessagePackObject]
public class FrameHeader public class FrameHeader
{ {
/// <summary> /// <summary>
/// The current accuracy of the score. /// The current accuracy of the score.
/// </summary> /// </summary>
[Key(0)]
public double Accuracy { get; set; } public double Accuracy { get; set; }
/// <summary> /// <summary>
/// The current combo of the score. /// The current combo of the score.
/// </summary> /// </summary>
[Key(1)]
public int Combo { get; set; } public int Combo { get; set; }
/// <summary> /// <summary>
/// The maximum combo achieved up to the current point in time. /// The maximum combo achieved up to the current point in time.
/// </summary> /// </summary>
[Key(2)]
public int MaxCombo { get; set; } public int MaxCombo { get; set; }
/// <summary> /// <summary>
/// Cumulative hit statistics. /// Cumulative hit statistics.
/// </summary> /// </summary>
[Key(3)]
public Dictionary<HitResult, int> Statistics { get; set; } public Dictionary<HitResult, int> Statistics { get; set; }
/// <summary> /// <summary>
/// The time at which this frame was received by the server. /// The time at which this frame was received by the server.
/// </summary> /// </summary>
[Key(4)]
public DateTimeOffset ReceivedTime { get; set; } public DateTimeOffset ReceivedTime { get; set; }
/// <summary> /// <summary>
@ -54,7 +61,8 @@ namespace osu.Game.Online.Spectator
} }
[JsonConstructor] [JsonConstructor]
public FrameHeader(int combo, int maxCombo, double accuracy, Dictionary<HitResult, int> statistics, DateTimeOffset receivedTime) [SerializationConstructor]
public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary<HitResult, int> statistics, DateTimeOffset receivedTime)
{ {
Combo = combo; Combo = combo;
MaxCombo = maxCombo; MaxCombo = maxCombo;

View File

@ -5,18 +5,23 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using MessagePack;
using osu.Game.Online.API; using osu.Game.Online.API;
namespace osu.Game.Online.Spectator namespace osu.Game.Online.Spectator
{ {
[Serializable] [Serializable]
[MessagePackObject]
public class SpectatorState : IEquatable<SpectatorState> public class SpectatorState : IEquatable<SpectatorState>
{ {
[Key(0)]
public int? BeatmapID { get; set; } public int? BeatmapID { get; set; }
[Key(1)]
public int? RulesetID { get; set; } public int? RulesetID { get; set; }
[NotNull] [NotNull]
[Key(2)]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>(); public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID; public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID;

View File

@ -10,6 +10,7 @@ using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -116,14 +117,19 @@ namespace osu.Game.Online.Spectator
if (connection != null) if (connection != null)
return; return;
connection = new HubConnectionBuilder() var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options => .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
{
options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
})
.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; })
.Build();
if (RuntimeInfo.SupportsJIT)
builder.AddMessagePackProtocol();
else
{
// eventually we will precompile resolvers for messagepack, but this isn't working currently
// see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
}
connection = builder.Build();
// until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198)
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);

View File

@ -327,6 +327,7 @@ namespace osu.Game
if (!SelectedMods.Disabled) if (!SelectedMods.Disabled)
SelectedMods.Value = Array.Empty<Mod>(); SelectedMods.Value = Array.Empty<Mod>();
AvailableMods.Value = dict; AvailableMods.Value = dict;
} }

View File

@ -12,7 +12,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
using Humanizer; using Humanizer;
using osu.Game.Utils; using osu.Framework.Extensions.EnumExtensions;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
@ -80,7 +80,7 @@ namespace osu.Game.Overlays.BeatmapListing
if (typeof(T).IsEnum) if (typeof(T).IsEnum)
{ {
foreach (var val in OrderAttributeUtils.GetValuesInOrder<T>()) foreach (var val in EnumExtensions.GetValuesInOrder<T>())
AddItem(val); AddItem(val);
} }
} }

View File

@ -1,7 +1,7 @@
// 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 osu.Game.Utils; using osu.Framework.Utils;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {

View File

@ -176,23 +176,34 @@ namespace osu.Game.Overlays
loadingLayer.Hide(); loadingLayer.Hide();
lastFetchDisplayedTime = Time.Current; lastFetchDisplayedTime = Time.Current;
if (content == currentContent)
return;
var lastContent = currentContent; var lastContent = currentContent;
if (lastContent != null) if (lastContent != null)
{ {
lastContent.FadeOut(100, Easing.OutQuint).Expire(); var transform = lastContent.FadeOut(100, Easing.OutQuint);
if (lastContent == notFoundContent)
{
// not found display may be used multiple times, so don't expire/dispose it.
transform.Schedule(() => panelTarget.Remove(lastContent));
}
else
{
// Consider the case when the new content is smaller than the last content. // Consider the case when the new content is smaller than the last content.
// If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird. // If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
// At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0. // At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
// To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so. // To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => panelTarget.Remove(lastContent)); lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => lastContent.Expire());
}
} }
if (!content.IsAlive) if (!content.IsAlive)
panelTarget.Add(content); panelTarget.Add(content);
content.FadeIn(200, Easing.OutQuint);
content.FadeInFromZero(200, Easing.OutQuint);
currentContent = content; currentContent = content;
} }
@ -202,7 +213,7 @@ namespace osu.Game.Overlays
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
private class NotFoundDrawable : CompositeDrawable public class NotFoundDrawable : CompositeDrawable
{ {
public NotFoundDrawable() public NotFoundDrawable()
{ {

View File

@ -1,25 +1,55 @@
// 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 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.Effects;
using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet namespace osu.Game.Overlays.BeatmapSet
{ {
public class BeatmapSetHeader : OverlayHeader public class BeatmapSetHeader : OverlayHeader
{ {
public readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>(); public readonly Bindable<BeatmapSetInfo> BeatmapSet = new Bindable<BeatmapSetInfo>();
public BeatmapSetHeaderContent HeaderContent { get; private set; }
[Cached]
public BeatmapRulesetSelector RulesetSelector { get; private set; } public BeatmapRulesetSelector RulesetSelector { get; private set; }
protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle(); [Cached(typeof(IBindable<RulesetInfo>))]
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
public BeatmapSetHeader()
{
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 3,
Offset = new Vector2(0f, 1f),
};
}
protected override Drawable CreateContent() => HeaderContent = new BeatmapSetHeaderContent
{
BeatmapSet = { BindTarget = BeatmapSet }
};
protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector
{ {
Current = Ruleset Current = ruleset
}; };
protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle();
private class BeatmapHeaderTitle : OverlayTitle private class BeatmapHeaderTitle : OverlayTitle
{ {
public BeatmapHeaderTitle() public BeatmapHeaderTitle()

View File

@ -3,12 +3,10 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
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.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -18,18 +16,21 @@ using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Overlays.BeatmapSet.Buttons;
using osu.Game.Rulesets;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet namespace osu.Game.Overlays.BeatmapSet
{ {
public class Header : BeatmapDownloadTrackingComposite public class BeatmapSetHeaderContent : BeatmapDownloadTrackingComposite
{ {
private const float transition_duration = 200; private const float transition_duration = 200;
private const float buttons_height = 45; private const float buttons_height = 45;
private const float buttons_spacing = 5; private const float buttons_spacing = 5;
public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
public readonly Details Details;
public readonly BeatmapPicker Picker;
private readonly UpdateableBeatmapSetCover cover; private readonly UpdateableBeatmapSetCover cover;
private readonly Box coverGradient; private readonly Box coverGradient;
private readonly OsuSpriteText title, artist; private readonly OsuSpriteText title, artist;
@ -38,52 +39,23 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly FillFlowContainer downloadButtonsContainer; private readonly FillFlowContainer downloadButtonsContainer;
private readonly BeatmapAvailability beatmapAvailability; private readonly BeatmapAvailability beatmapAvailability;
private readonly BeatmapSetOnlineStatusPill onlineStatusPill; private readonly BeatmapSetOnlineStatusPill onlineStatusPill;
public Details Details; private readonly FavouriteButton favouriteButton;
private readonly FillFlowContainer fadeContent;
public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); private readonly LoadingSpinner loading;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
public BeatmapRulesetSelector RulesetSelector => beatmapSetHeader.RulesetSelector; [Resolved]
public readonly BeatmapPicker Picker; private BeatmapRulesetSelector rulesetSelector { get; set; }
private readonly FavouriteButton favouriteButton; public BeatmapSetHeaderContent()
private readonly FillFlowContainer fadeContent;
private readonly LoadingSpinner loading;
private readonly BeatmapSetHeader beatmapSetHeader;
[Cached(typeof(IBindable<RulesetInfo>))]
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
public Header()
{ {
ExternalLinkButton externalLink; ExternalLinkButton externalLink;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Masking = true; InternalChild = new Container
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 3,
Offset = new Vector2(0f, 1f),
};
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
beatmapSetHeader = new BeatmapSetHeader
{
Ruleset = { BindTarget = ruleset },
},
new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -217,8 +189,6 @@ namespace osu.Game.Overlays.BeatmapSet
Details = new Details(), Details = new Details(),
}, },
}, },
},
},
} }
}; };
@ -239,7 +209,7 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.BindValueChanged(setInfo => BeatmapSet.BindValueChanged(setInfo =>
{ {
Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
cover.BeatmapSet = setInfo.NewValue; cover.BeatmapSet = setInfo.NewValue;
if (setInfo.NewValue == null) if (setInfo.NewValue == null)

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
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;
@ -15,7 +16,6 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -105,7 +105,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
var ruleset = scores.First().Ruleset.CreateInstance(); var ruleset = scores.First().Ruleset.CreateInstance();
foreach (var result in OrderAttributeUtils.GetValuesInOrder<HitResult>()) foreach (var result in EnumExtensions.GetValuesInOrder<HitResult>())
{ {
if (!allScoreStatistics.Contains(result)) if (!allScoreStatistics.Contains(result))
continue; continue;

View File

@ -19,15 +19,12 @@ using osuTK;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class BeatmapSetOverlay : FullscreenOverlay<OverlayHeader> // we don't provide a standard header for now. public class BeatmapSetOverlay : FullscreenOverlay<BeatmapSetHeader>
{ {
public const float X_PADDING = 40; public const float X_PADDING = 40;
public const float Y_PADDING = 25; public const float Y_PADDING = 25;
public const float RIGHT_WIDTH = 275; public const float RIGHT_WIDTH = 275;
//todo: should be an OverlayHeader? or maybe not?
protected new readonly Header Header;
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
@ -39,7 +36,7 @@ namespace osu.Game.Overlays
private readonly Box background; private readonly Box background;
public BeatmapSetOverlay() public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue, null) : base(OverlayColourScheme.Blue, new BeatmapSetHeader())
{ {
OverlayScrollContainer scroll; OverlayScrollContainer scroll;
Info info; Info info;
@ -72,14 +69,14 @@ namespace osu.Game.Overlays
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
Header = new Header(), Header,
info = new Info() info = new Info()
} }
}, },
}, },
new ScoresContainer new ScoresContainer
{ {
Beatmap = { BindTarget = Header.Picker.Beatmap } Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
}, },
comments = new CommentsSection() comments = new CommentsSection()
}, },
@ -91,7 +88,7 @@ namespace osu.Game.Overlays
info.BeatmapSet.BindTo(beatmapSet); info.BeatmapSet.BindTo(beatmapSet);
comments.BeatmapSet.BindTo(beatmapSet); comments.BeatmapSet.BindTo(beatmapSet);
Header.Picker.Beatmap.ValueChanged += b => Header.HeaderContent.Picker.Beatmap.ValueChanged += b =>
{ {
info.Beatmap = b.NewValue; info.Beatmap = b.NewValue;
@ -125,7 +122,7 @@ namespace osu.Game.Overlays
req.Success += res => req.Success += res =>
{ {
beatmapSet.Value = res.ToBeatmapSet(rulesets); beatmapSet.Value = res.ToBeatmapSet(rulesets);
Header.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId); Header.HeaderContent.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId);
}; };
API.Queue(req); API.Queue(req);

View File

@ -190,13 +190,13 @@ namespace osu.Game.Overlays.Chat
} }
} }
}; };
updateMessageContent();
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
updateMessageContent();
FinishTransforms(true); FinishTransforms(true);
} }

View File

@ -33,11 +33,16 @@ namespace osu.Game.Overlays.Comments
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved(canBeNull: true)]
private LoginOverlay login { get; set; }
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } private OverlayColourProvider colourProvider { get; set; }
protected Box Background { get; private set; }
private readonly Comment comment; private readonly Comment comment;
private Box background;
private Box hoverLayer; private Box hoverLayer;
private CircularContainer borderContainer; private CircularContainer borderContainer;
private SpriteText sideNumber; private SpriteText sideNumber;
@ -62,8 +67,12 @@ namespace osu.Game.Overlays.Comments
AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight; AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight;
hoverLayer.Colour = Color4.Black.Opacity(0.5f); hoverLayer.Colour = Color4.Black.Opacity(0.5f);
if (api.IsLoggedIn && api.LocalUser.Value.Id != comment.UserId) var ownComment = api.LocalUser.Value.Id == comment.UserId;
if (!ownComment)
Action = onAction; Action = onAction;
Background.Alpha = ownComment ? 0 : 1;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -71,12 +80,18 @@ namespace osu.Game.Overlays.Comments
base.LoadComplete(); base.LoadComplete();
isVoted.Value = comment.IsVoted; isVoted.Value = comment.IsVoted;
votesCount.Value = comment.VotesCount; votesCount.Value = comment.VotesCount;
isVoted.BindValueChanged(voted => background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true); isVoted.BindValueChanged(voted => Background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true);
votesCount.BindValueChanged(count => votesCounter.Text = $"+{count.NewValue}", true); votesCount.BindValueChanged(count => votesCounter.Text = $"+{count.NewValue}", true);
} }
private void onAction() private void onAction()
{ {
if (!api.IsLoggedIn)
{
login?.Show();
return;
}
request = new CommentVoteRequest(comment.Id, isVoted.Value ? CommentVoteAction.UnVote : CommentVoteAction.Vote); request = new CommentVoteRequest(comment.Id, isVoted.Value ? CommentVoteAction.UnVote : CommentVoteAction.Vote);
request.Success += onSuccess; request.Success += onSuccess;
api.Queue(request); api.Queue(request);
@ -102,7 +117,7 @@ namespace osu.Game.Overlays.Comments
Masking = true, Masking = true,
Children = new Drawable[] Children = new Drawable[]
{ {
background = new Box Background = new Box
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },

View File

@ -236,13 +236,13 @@ namespace osu.Game.Overlays.Mods
{ {
iconsContainer.AddRange(new[] iconsContainer.AddRange(new[]
{ {
backgroundIcon = new PassThroughTooltipModIcon(Mods[1]) backgroundIcon = new ModIcon(Mods[1], false)
{ {
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Position = new Vector2(1.5f), Position = new Vector2(1.5f),
}, },
foregroundIcon = new PassThroughTooltipModIcon(Mods[0]) foregroundIcon = new ModIcon(Mods[0], false)
{ {
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
@ -252,7 +252,7 @@ namespace osu.Game.Overlays.Mods
} }
else else
{ {
iconsContainer.Add(foregroundIcon = new PassThroughTooltipModIcon(Mod) iconsContainer.Add(foregroundIcon = new ModIcon(Mod, false)
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -297,15 +297,5 @@ namespace osu.Game.Overlays.Mods
Mod = mod; Mod = mod;
} }
private class PassThroughTooltipModIcon : ModIcon
{
public override string TooltipText => null;
public PassThroughTooltipModIcon(Mod mod)
: base(mod)
{
}
}
} }
} }

View File

@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Mods
{ {
public class ModSelectOverlay : WaveOverlayContainer public class ModSelectOverlay : WaveOverlayContainer
{ {
private readonly Func<Mod, bool> isValidMod;
public const float HEIGHT = 510; public const float HEIGHT = 510;
protected readonly TriangleButton DeselectAllButton; protected readonly TriangleButton DeselectAllButton;
@ -60,8 +61,10 @@ namespace osu.Game.Overlays.Mods
private SampleChannel sampleOn, sampleOff; private SampleChannel sampleOn, sampleOff;
public ModSelectOverlay() public ModSelectOverlay(Func<Mod, bool> isValidMod = null)
{ {
this.isValidMod = isValidMod ?? (m => true);
Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2"); Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2");
Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2"); Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2");
Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774"); Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774");
@ -213,9 +216,9 @@ namespace osu.Game.Overlays.Mods
}, },
new Drawable[] new Drawable[]
{ {
// Footer
new Container new Container
{ {
Name = "Footer content",
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
@ -234,10 +237,9 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
RelativePositionAxes = Axes.X,
Width = content_width, Width = content_width,
Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2), Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2),
LayoutDuration = 100,
LayoutEasing = Easing.OutQuint,
Padding = new MarginPadding Padding = new MarginPadding
{ {
Vertical = 15, Vertical = 15,
@ -351,7 +353,7 @@ namespace osu.Game.Overlays.Mods
{ {
base.PopOut(); base.PopOut();
footerContainer.MoveToX(footerContainer.DrawSize.X, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); footerContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
foreach (var section in ModSectionsContainer.Children) foreach (var section in ModSectionsContainer.Children)
@ -403,7 +405,7 @@ namespace osu.Game.Overlays.Mods
if (mods.NewValue == null) return; if (mods.NewValue == null) return;
foreach (var section in ModSectionsContainer.Children) foreach (var section in ModSectionsContainer.Children)
section.Mods = mods.NewValue[section.ModType]; section.Mods = mods.NewValue[section.ModType].Where(isValidMod);
} }
private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods) private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)

View File

@ -17,9 +17,9 @@ using osuTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
/// <summary> /// <summary>
/// <see cref="OsuScrollContainer"/> which provides <see cref="ScrollToTopButton"/>. Mostly used in <see cref="FullscreenOverlay{T}"/>. /// <see cref="UserTrackingScrollContainer"/> which provides <see cref="ScrollToTopButton"/>. Mostly used in <see cref="FullscreenOverlay{T}"/>.
/// </summary> /// </summary>
public class OverlayScrollContainer : OsuScrollContainer public class OverlayScrollContainer : UserTrackingScrollContainer
{ {
/// <summary> /// <summary>
/// Scroll position at which the <see cref="ScrollToTopButton"/> will be shown. /// Scroll position at which the <see cref="ScrollToTopButton"/> will be shown.

View File

@ -49,9 +49,12 @@ namespace osu.Game.Overlays.Profile.Header
Spacing = new Vector2(10, 0), Spacing = new Vector2(10, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
new AddFriendButton new FollowersButton
{
User = { BindTarget = User }
},
new MappingSubscribersButton
{ {
RelativeSizeAxes = Axes.Y,
User = { BindTarget = User } User = { BindTarget = User }
}, },
new MessageUserButton new MessageUserButton
@ -69,7 +72,6 @@ namespace osu.Game.Overlays.Profile.Header
Width = UserProfileOverlay.CONTENT_X_MARGIN, Width = UserProfileOverlay.CONTENT_X_MARGIN,
Child = new ExpandDetailsButton Child = new ExpandDetailsButton
{ {
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
DetailsVisible = { BindTarget = DetailsVisible } DetailsVisible = { BindTarget = DetailsVisible }

View File

@ -1,60 +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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
{
public class AddFriendButton : ProfileHeaderButton
{
public readonly Bindable<User> User = new Bindable<User>();
public override string TooltipText => "friends";
private OsuSpriteText followerText;
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Right = 10 },
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.User,
FillMode = FillMode.Fit,
Size = new Vector2(50, 14)
},
followerText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Bold)
}
}
};
// todo: when friending/unfriending is implemented, the APIAccess.Friends list should be updated accordingly.
User.BindValueChanged(user => updateFollowers(user.NewValue), true);
}
private void updateFollowers(User user) => followerText.Text = user?.FollowerCount.ToString("#,##0");
}
}

View File

@ -0,0 +1,26 @@
// 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.Sprites;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
{
public class FollowersButton : ProfileHeaderStatisticsButton
{
public readonly Bindable<User> User = new Bindable<User>();
public override string TooltipText => "followers";
protected override IconUsage Icon => FontAwesome.Solid.User;
[BackgroundDependencyLoader]
private void load()
{
// todo: when friending/unfriending is implemented, the APIAccess.Friends list should be updated accordingly.
User.BindValueChanged(user => SetValue(user.NewValue?.FollowerCount ?? 0), true);
}
}
}

View File

@ -0,0 +1,25 @@
// 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.Sprites;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
{
public class MappingSubscribersButton : ProfileHeaderStatisticsButton
{
public readonly Bindable<User> User = new Bindable<User>();
public override string TooltipText => "mapping subscribers";
protected override IconUsage Icon => FontAwesome.Solid.Bell;
[BackgroundDependencyLoader]
private void load()
{
User.BindValueChanged(user => SetValue(user.NewValue?.MappingFollowerCount ?? 0), true);
}
}
}

View File

@ -33,7 +33,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
public MessageUserButton() public MessageUserButton()
{ {
Content.Alpha = 0; Content.Alpha = 0;
RelativeSizeAxes = Axes.Y;
Child = new SpriteIcon Child = new SpriteIcon
{ {

View File

@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
protected ProfileHeaderButton() protected ProfileHeaderButton()
{ {
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
Height = 40;
base.Content.Add(new CircularContainer base.Content.Add(new CircularContainer
{ {

View File

@ -0,0 +1,51 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
{
public abstract class ProfileHeaderStatisticsButton : ProfileHeaderButton
{
private readonly OsuSpriteText drawableText;
protected ProfileHeaderStatisticsButton()
{
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = Icon,
FillMode = FillMode.Fit,
Size = new Vector2(50, 14)
},
drawableText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 10 },
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold)
}
}
};
}
protected abstract IconUsage Icon { get; }
protected void SetValue(int value) => drawableText.Text = value.ToString("#,##0");
}
}

View File

@ -38,6 +38,18 @@ namespace osu.Game.Overlays.Settings.Sections
private List<SkinInfo> skinItems; private List<SkinInfo> skinItems;
private int firstNonDefaultSkinIndex
{
get
{
var index = skinItems.FindIndex(s => s.ID > 0);
if (index < 0)
index = skinItems.Count;
return index;
}
}
[Resolved] [Resolved]
private SkinManager skins { get; set; } private SkinManager skins { get; set; }
@ -96,7 +108,7 @@ namespace osu.Game.Overlays.Settings.Sections
if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) if (skinDropdown.Items.All(s => s.ID != configBindable.Value))
configBindable.Value = 0; configBindable.Value = 0;
configBindable.BindValueChanged(id => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == id.NewValue), true); configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig), true);
dropdownBindable.BindValueChanged(skin => dropdownBindable.BindValueChanged(skin =>
{ {
if (skin.NewValue == random_skin_info) if (skin.NewValue == random_skin_info)
@ -109,24 +121,42 @@ namespace osu.Game.Overlays.Settings.Sections
}); });
} }
private void updateSelectedSkinFromConfig()
{
int id = configBindable.Value;
var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == id);
if (skin == null)
{
// there may be a thread race condition where an item is selected that hasn't yet been added to the dropdown.
// to avoid adding complexity, let's just ensure the item is added so we can perform the selection.
skin = skins.Query(s => s.ID == id);
addItem(skin);
}
dropdownBindable.Value = skin;
}
private void updateItems() private void updateItems()
{ {
skinItems = skins.GetAllUsableSkins(); skinItems = skins.GetAllUsableSkins();
skinItems.Insert(firstNonDefaultSkinIndex, random_skin_info);
// insert after lazer built-in skins sortUserSkins(skinItems);
int firstNonDefault = skinItems.FindIndex(s => s.ID > 0);
if (firstNonDefault < 0)
firstNonDefault = skinItems.Count;
skinItems.Insert(firstNonDefault, random_skin_info);
skinDropdown.Items = skinItems; skinDropdown.Items = skinItems;
} }
private void itemUpdated(ValueChangedEvent<WeakReference<SkinInfo>> weakItem) private void itemUpdated(ValueChangedEvent<WeakReference<SkinInfo>> weakItem)
{ {
if (weakItem.NewValue.TryGetTarget(out var item)) if (weakItem.NewValue.TryGetTarget(out var item))
Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToArray()); Schedule(() => addItem(item));
}
private void addItem(SkinInfo item)
{
List<SkinInfo> newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList();
sortUserSkins(newDropdownItems);
skinDropdown.Items = newDropdownItems;
} }
private void itemRemoved(ValueChangedEvent<WeakReference<SkinInfo>> weakItem) private void itemRemoved(ValueChangedEvent<WeakReference<SkinInfo>> weakItem)
@ -135,6 +165,13 @@ namespace osu.Game.Overlays.Settings.Sections
Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray());
} }
private void sortUserSkins(List<SkinInfo> skinsList)
{
// Sort user skins separately from built-in skins
skinsList.Sort(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex,
Comparer<SkinInfo>.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)));
}
private class SkinSettingsDropdown : SettingsDropdown<SkinInfo> private class SkinSettingsDropdown : SettingsDropdown<SkinInfo>
{ {
protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl(); protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl();

View File

@ -202,7 +202,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
protected override OsuScrollContainer CreateScrollContainer() => new OverlayScrollContainer(); protected override UserTrackingScrollContainer CreateScrollContainer() => new OverlayScrollContainer();
protected override FlowContainer<ProfileSection> CreateScrollContentContainer() => new FillFlowContainer<ProfileSection> protected override FlowContainer<ProfileSection> CreateScrollContentContainer() => new FillFlowContainer<ProfileSection>
{ {

View File

@ -1,38 +1,51 @@
// 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 MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osuTK; using osuTK;
namespace osu.Game.Replays.Legacy namespace osu.Game.Replays.Legacy
{ {
[MessagePackObject]
public class LegacyReplayFrame : ReplayFrame public class LegacyReplayFrame : ReplayFrame
{ {
[JsonIgnore] [JsonIgnore]
[IgnoreMember]
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
[Key(1)]
public float? MouseX; public float? MouseX;
[Key(2)]
public float? MouseY; public float? MouseY;
[JsonIgnore] [JsonIgnore]
[IgnoreMember]
public bool MouseLeft => MouseLeft1 || MouseLeft2; public bool MouseLeft => MouseLeft1 || MouseLeft2;
[JsonIgnore] [JsonIgnore]
[IgnoreMember]
public bool MouseRight => MouseRight1 || MouseRight2; public bool MouseRight => MouseRight1 || MouseRight2;
[JsonIgnore] [JsonIgnore]
[IgnoreMember]
public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1); public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1);
[JsonIgnore] [JsonIgnore]
[IgnoreMember]
public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1); public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1);
[JsonIgnore] [JsonIgnore]
[IgnoreMember]
public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2); public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2);
[JsonIgnore] [JsonIgnore]
[IgnoreMember]
public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2); public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2);
[Key(3)]
public ReplayButtonState ButtonState; public ReplayButtonState ButtonState;
public LegacyReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState) public LegacyReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState)

View File

@ -332,7 +332,7 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap.Add(hitObject); EditorBeatmap.Add(hitObject);
if (EditorClock.CurrentTime < hitObject.StartTime) if (EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekTo(hitObject.StartTime); EditorClock.SeekSmoothlyTo(hitObject.StartTime);
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods
{ {
} }
public void ApplyToDifficulty(BeatmapDifficulty difficulty) public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty)
{ {
const float ratio = 1.4f; const float ratio = 1.4f;
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.

View File

@ -1,10 +1,14 @@
// 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 MessagePack;
namespace osu.Game.Rulesets.Replays namespace osu.Game.Rulesets.Replays
{ {
[MessagePackObject]
public class ReplayFrame public class ReplayFrame
{ {
[Key(0)]
public double Time; public double Time;
public ReplayFrame() public ReplayFrame()

View File

@ -24,9 +24,9 @@ using osu.Game.Skinning;
using osu.Game.Users; using osu.Game.Users;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Utils;
namespace osu.Game.Rulesets namespace osu.Game.Rulesets
{ {
@ -272,7 +272,7 @@ namespace osu.Game.Rulesets
var validResults = GetValidHitResults(); var validResults = GetValidHitResults();
// enumerate over ordered list to guarantee return order is stable. // enumerate over ordered list to guarantee return order is stable.
foreach (var result in OrderAttributeUtils.GetValuesInOrder<HitResult>()) foreach (var result in EnumExtensions.GetValuesInOrder<HitResult>())
{ {
switch (result) switch (result)
{ {
@ -298,7 +298,7 @@ namespace osu.Game.Rulesets
/// <remarks> /// <remarks>
/// <see cref="HitResult.Miss"/> is implicitly included. Special types like <see cref="HitResult.IgnoreHit"/> are ignored even when specified. /// <see cref="HitResult.Miss"/> is implicitly included. Special types like <see cref="HitResult.IgnoreHit"/> are ignored even when specified.
/// </remarks> /// </remarks>
protected virtual IEnumerable<HitResult> GetValidHitResults() => OrderAttributeUtils.GetValuesInOrder<HitResult>(); protected virtual IEnumerable<HitResult> GetValidHitResults() => EnumExtensions.GetValuesInOrder<HitResult>();
/// <summary> /// <summary>
/// Get a display friendly name for the specified result type. /// Get a display friendly name for the specified result type.

View File

@ -3,7 +3,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using osu.Game.Utils; using osu.Framework.Utils;
namespace osu.Game.Rulesets.Scoring namespace osu.Game.Rulesets.Scoring
{ {

View File

@ -124,9 +124,11 @@ namespace osu.Game.Rulesets.UI
Debug.Assert(drawableMap.ContainsKey(entry)); Debug.Assert(drawableMap.ContainsKey(entry));
var drawable = drawableMap[entry]; var drawable = drawableMap[entry];
// OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding.
drawable.OnKilled();
drawable.OnNewResult -= onNewResult; drawable.OnNewResult -= onNewResult;
drawable.OnRevertResult -= onRevertResult; drawable.OnRevertResult -= onRevertResult;
drawable.OnKilled();
drawableMap.Remove(entry); drawableMap.Remove(entry);

View File

@ -16,6 +16,9 @@ using osu.Framework.Bindables;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
/// <summary>
/// Display the specified mod at a fixed size.
/// </summary>
public class ModIcon : Container, IHasTooltip public class ModIcon : Container, IHasTooltip
{ {
public readonly BindableBool Selected = new BindableBool(); public readonly BindableBool Selected = new BindableBool();
@ -28,9 +31,10 @@ namespace osu.Game.Rulesets.UI
private readonly ModType type; private readonly ModType type;
public virtual string TooltipText => mod.IconTooltip; public virtual string TooltipText => showTooltip ? mod.IconTooltip : null;
private Mod mod; private Mod mod;
private readonly bool showTooltip;
public Mod Mod public Mod Mod
{ {
@ -42,9 +46,15 @@ namespace osu.Game.Rulesets.UI
} }
} }
public ModIcon(Mod mod) /// <summary>
/// Construct a new instance.
/// </summary>
/// <param name="mod">The mod to be displayed</param>
/// <param name="showTooltip">Whether a tooltip describing the mod should display on hover.</param>
public ModIcon(Mod mod, bool showTooltip = true)
{ {
this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); this.mod = mod ?? throw new ArgumentNullException(nameof(mod));
this.showTooltip = showTooltip;
type = mod.Type; type = mod.Type;

View File

@ -2,7 +2,6 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
@ -13,7 +12,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary> /// </summary>
public class BookmarkPart : TimelinePart public class BookmarkPart : TimelinePart
{ {
protected override void LoadBeatmap(WorkingBeatmap beatmap) protected override void LoadBeatmap(EditorBeatmap beatmap)
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
foreach (int bookmark in beatmap.BeatmapInfo.Bookmarks) foreach (int bookmark in beatmap.BeatmapInfo.Bookmarks)

View File

@ -2,7 +2,6 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
@ -14,10 +13,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary> /// </summary>
public class BreakPart : TimelinePart public class BreakPart : TimelinePart
{ {
protected override void LoadBeatmap(WorkingBeatmap beatmap) protected override void LoadBeatmap(EditorBeatmap beatmap)
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
foreach (var breakPeriod in beatmap.Beatmap.Breaks) foreach (var breakPeriod in beatmap.Breaks)
Add(new BreakVisualisation(breakPeriod)); Add(new BreakVisualisation(breakPeriod));
} }

View File

@ -4,7 +4,6 @@
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
@ -16,12 +15,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>(); private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
protected override void LoadBeatmap(WorkingBeatmap beatmap) protected override void LoadBeatmap(EditorBeatmap beatmap)
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
controlPointGroups.UnbindAll(); controlPointGroups.UnbindAll();
controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) => controlPointGroups.BindCollectionChanged((sender, args) =>
{ {
switch (args.Action) switch (args.Action)

View File

@ -2,15 +2,14 @@
// 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;
using osuTK;
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.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
@ -54,11 +53,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
scheduledSeek?.Cancel(); scheduledSeek?.Cancel();
scheduledSeek = Schedule(() => scheduledSeek = Schedule(() =>
{ {
if (Beatmap.Value == null)
return;
float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth);
editorClock.SeekTo(markerPos / DrawWidth * editorClock.TrackLength); editorClock.SeekSmoothlyTo(markerPos / DrawWidth * editorClock.TrackLength);
}); });
} }
@ -68,7 +64,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
marker.X = (float)editorClock.CurrentTime; marker.X = (float)editorClock.CurrentTime;
} }
protected override void LoadBeatmap(WorkingBeatmap beatmap) protected override void LoadBeatmap(EditorBeatmap beatmap)
{ {
// block base call so we don't clear our marker (can be reused on beatmap change). // block base call so we don't clear our marker (can be reused on beatmap change).
} }

View File

@ -21,7 +21,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary> /// </summary>
public class TimelinePart<T> : Container<T> where T : Drawable public class TimelinePart<T> : Container<T> where T : Drawable
{ {
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
protected readonly IBindable<Track> Track = new Bindable<Track>(); protected readonly IBindable<Track> Track = new Bindable<Track>();
@ -33,10 +36,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
AddInternal(this.content = content ?? new Container<T> { RelativeSizeAxes = Axes.Both }); AddInternal(this.content = content ?? new Container<T> { RelativeSizeAxes = Axes.Both });
Beatmap.ValueChanged += b => beatmap.ValueChanged += b =>
{ {
updateRelativeChildSize(); updateRelativeChildSize();
LoadBeatmap(b.NewValue);
}; };
Track.ValueChanged += _ => updateRelativeChildSize(); Track.ValueChanged += _ => updateRelativeChildSize();
@ -45,24 +47,26 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock) private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock)
{ {
Beatmap.BindTo(beatmap); this.beatmap.BindTo(beatmap);
LoadBeatmap(EditorBeatmap);
Track.BindTo(clock.Track); Track.BindTo(clock.Track);
} }
private void updateRelativeChildSize() private void updateRelativeChildSize()
{ {
// the track may not be loaded completely (only has a length once it is). // the track may not be loaded completely (only has a length once it is).
if (!Beatmap.Value.Track.IsLoaded) if (!beatmap.Value.Track.IsLoaded)
{ {
content.RelativeChildSize = Vector2.One; content.RelativeChildSize = Vector2.One;
Schedule(updateRelativeChildSize); Schedule(updateRelativeChildSize);
return; return;
} }
content.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); content.RelativeChildSize = new Vector2((float)Math.Max(1, beatmap.Value.Track.Length), 1);
} }
protected virtual void LoadBeatmap(WorkingBeatmap beatmap) protected virtual void LoadBeatmap(EditorBeatmap beatmap)
{ {
content.Clear(); content.Clear();
} }

View File

@ -170,7 +170,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
return false; return false;
EditorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); EditorClock?.SeekSmoothlyTo(clickedBlueprint.HitObject.StartTime);
return true; return true;
} }

View File

@ -155,12 +155,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
seekTrackToCurrent(); seekTrackToCurrent();
else if (!editorClock.IsRunning) else if (!editorClock.IsRunning)
{ {
// The track isn't running. There are two cases we have to be wary of: // The track isn't running. There are three cases we have to be wary of:
// 1) The user flick-drags on this timeline: We want the track to follow us // 1) The user flick-drags on this timeline and we are applying an interpolated seek on the clock, until interrupted by 2 or 3.
// 2) The user changes the track time through some other means (scrolling in the editor or overview timeline): We want to follow the track time // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline; clicking a hitobject etc.). We want the timeline to track the clock's time.
// 3) An ongoing seek transform is running from an external seek. We want the timeline to track the clock's time.
// The simplest way to cover both cases is by checking whether the scroll position has changed and the audio hasn't been changed externally // The simplest way to cover the first two cases is by checking whether the scroll position has changed and the audio hasn't been changed externally
if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime) // Checking IsSeeking covers the third case, where the transform may not have been applied yet.
if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime && !editorClock.IsSeeking)
seekTrackToCurrent(); seekTrackToCurrent();
else else
scrollToTrackTime(); scrollToTrackTime();

View File

@ -5,7 +5,6 @@ using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@ -23,12 +22,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
protected override void LoadBeatmap(WorkingBeatmap beatmap) protected override void LoadBeatmap(EditorBeatmap beatmap)
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
controlPointGroups.UnbindAll(); controlPointGroups.UnbindAll();
controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) => controlPointGroups.BindCollectionChanged((sender, args) =>
{ {
switch (args.Action) switch (args.Action)

View File

@ -131,6 +131,10 @@ namespace osu.Game.Screens.Edit
try try
{ {
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
// clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages.
// eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases.
playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.CreateCopy();
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -74,7 +74,11 @@ namespace osu.Game.Screens.Edit
public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; public BeatmapMetadata Metadata => PlayableBeatmap.Metadata;
public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; public ControlPointInfo ControlPointInfo
{
get => PlayableBeatmap.ControlPointInfo;
set => PlayableBeatmap.ControlPointInfo = value;
}
public List<BreakPeriod> Breaks => PlayableBeatmap.Breaks; public List<BreakPeriod> Breaks => PlayableBeatmap.Breaks;

View File

@ -35,6 +35,11 @@ namespace osu.Game.Screens.Edit
private readonly Bindable<bool> seekingOrStopped = new Bindable<bool>(true); private readonly Bindable<bool> seekingOrStopped = new Bindable<bool>(true);
/// <summary>
/// Whether a seek is currently in progress. True for the duration of a seek performed via <see cref="SeekSmoothlyTo"/>.
/// </summary>
public bool IsSeeking { get; private set; }
public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
: this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor)
{ {
@ -111,7 +116,7 @@ namespace osu.Game.Screens.Edit
if (!snapped || ControlPointInfo.TimingPoints.Count == 0) if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
{ {
SeekTo(seekTime); SeekSmoothlyTo(seekTime);
return; return;
} }
@ -145,11 +150,11 @@ namespace osu.Game.Screens.Edit
// Ensure the sought point is within the boundaries // Ensure the sought point is within the boundaries
seekTime = Math.Clamp(seekTime, 0, TrackLength); seekTime = Math.Clamp(seekTime, 0, TrackLength);
SeekTo(seekTime); SeekSmoothlyTo(seekTime);
} }
/// <summary> /// <summary>
/// The current time of this clock, include any active transform seeks performed via <see cref="SeekTo"/>. /// The current time of this clock, include any active transform seeks performed via <see cref="SeekSmoothlyTo"/>.
/// </summary> /// </summary>
public double CurrentTimeAccurate => public double CurrentTimeAccurate =>
Transforms.OfType<TransformSeek>().FirstOrDefault()?.EndValue ?? CurrentTime; Transforms.OfType<TransformSeek>().FirstOrDefault()?.EndValue ?? CurrentTime;
@ -176,12 +181,29 @@ namespace osu.Game.Screens.Edit
public bool Seek(double position) public bool Seek(double position)
{ {
seekingOrStopped.Value = true; seekingOrStopped.Value = IsSeeking = true;
ClearTransforms(); ClearTransforms();
return underlyingClock.Seek(position); return underlyingClock.Seek(position);
} }
/// <summary>
/// Seek smoothly to the provided destination.
/// Use <see cref="Seek"/> to perform an immediate seek.
/// </summary>
/// <param name="seekDestination"></param>
public void SeekSmoothlyTo(double seekDestination)
{
seekingOrStopped.Value = true;
if (IsRunning)
Seek(seekDestination);
else
{
transformSeekTo(seekDestination, transform_time, Easing.OutQuint);
}
}
public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments(); public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments();
double IAdjustableClock.Rate double IAdjustableClock.Rate
@ -229,6 +251,8 @@ namespace osu.Game.Screens.Edit
{ {
if (seekingOrStopped.Value) if (seekingOrStopped.Value)
{ {
IsSeeking &= Transforms.Any();
if (track.Value?.IsRunning != true) if (track.Value?.IsRunning != true)
{ {
// seeking in the editor can happen while the track isn't running. // seeking in the editor can happen while the track isn't running.
@ -239,20 +263,10 @@ namespace osu.Game.Screens.Edit
// we are either running a seek tween or doing an immediate seek. // we are either running a seek tween or doing an immediate seek.
// in the case of an immediate seek the seeking bool will be set to false after one update. // in the case of an immediate seek the seeking bool will be set to false after one update.
// this allows for silencing hit sounds and the likes. // this allows for silencing hit sounds and the likes.
seekingOrStopped.Value = Transforms.Any(); seekingOrStopped.Value = IsSeeking;
} }
} }
public void SeekTo(double seekDestination)
{
seekingOrStopped.Value = true;
if (IsRunning)
Seek(seekDestination);
else
transformSeekTo(seekDestination, transform_time, Easing.OutQuint);
}
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(), seek, duration, easing));

View File

@ -206,7 +206,7 @@ namespace osu.Game.Screens.Edit.Timing
Action = () => Action = () =>
{ {
selectedGroup.Value = controlGroup; selectedGroup.Value = controlGroup;
clock.SeekTo(controlGroup.Time); clock.SeekSmoothlyTo(controlGroup.Time);
}; };
} }

View File

@ -38,5 +38,11 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#lazermp_{roomId.Value}" }); Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#lazermp_{roomId.Value}" });
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
channelManager?.LeaveChannel(Channel.Value);
}
} }
} }

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@ -23,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.OnResuming(last); base.OnResuming(last);
if (client.Room != null) if (client.Room != null)
client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); client.ChangeState(MultiplayerUserState.Idle);
} }
protected override void UpdatePollingRate(bool isIdle) protected override void UpdatePollingRate(bool isIdle)

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
@ -109,5 +110,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
} }
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(isValidMod);
private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true;
} }
} }

View File

@ -11,7 +11,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Extensions;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
@ -44,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[CanBeNull] [CanBeNull]
private IDisposable readyClickOperation; private IDisposable readyClickOperation;
private GridContainer mainContent;
public MultiplayerMatchSubScreen(Room room) public MultiplayerMatchSubScreen(Room room)
{ {
Title = room.RoomID.Value == null ? "New room" : room.Name.Value; Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
@ -55,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new GridContainer mainContent = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Content = new[] Content = new[]
@ -178,6 +179,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden }
} }
}; };
if (client.Room == null)
{
// A new room is being created.
// The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed.
mainContent.Hide();
settingsOverlay.State.BindValueChanged(visibility =>
{
if (visibility.NewValue == Visibility.Hidden)
mainContent.Show();
}, true);
}
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -222,7 +236,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// accessing Exception here silences any potential errors from the antecedent task // accessing Exception here silences any potential errors from the antecedent task
if (t.Exception != null) if (t.Exception != null)
{ {
t.CatchUnobservedExceptions(true); // will run immediately.
// gameplay was not started due to an exception; unblock button. // gameplay was not started due to an exception; unblock button.
endOperation(); endOperation();
} }
@ -233,11 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
} }
client.ToggleReady() client.ToggleReady()
.ContinueWith(t => .ContinueWith(t => endOperation());
{
t.CatchUnobservedExceptions(true); // will run immediately.
endOperation();
});
void endOperation() void endOperation()
{ {

View File

@ -9,7 +9,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Extensions;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Online.Rooms.RoomStatuses;
@ -69,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.PartRoom(); base.PartRoom();
multiplayerClient.LeaveRoom().CatchUnobservedExceptions(); multiplayerClient.LeaveRoom();
// Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case.
// This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling.

View File

@ -10,7 +10,6 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Extensions;
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;
@ -176,7 +175,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (Room.Host?.UserID != api.LocalUser.Value.Id) if (Room.Host?.UserID != api.LocalUser.Value.Id)
return; return;
Client.TransferHost(targetUser).CatchUnobservedExceptions(true); Client.TransferHost(targetUser);
}) })
}; };
} }

View File

@ -2,7 +2,6 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -43,17 +42,46 @@ namespace osu.Game.Screens.OnlinePlay
leasedInProgress = inProgress.BeginLease(true); leasedInProgress = inProgress.BeginLease(true);
leasedInProgress.Value = true; leasedInProgress.Value = true;
// for extra safety, marshal the end of operation back to the update thread if necessary. return new OngoingOperation(this, leasedInProgress);
return new InvokeOnDisposal(() => Scheduler.Add(endOperation, false));
} }
private void endOperation() private void endOperationWithKnownLease(LeasedBindable<bool> lease)
{ {
if (leasedInProgress == null) if (lease != leasedInProgress)
throw new InvalidOperationException("Cannot end operation multiple times."); return;
leasedInProgress.Return(); // for extra safety, marshal the end of operation back to the update thread if necessary.
Scheduler.Add(() =>
{
leasedInProgress?.Return();
leasedInProgress = null;
}, false);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
// base call does an UnbindAllBindables().
// clean up the leased reference here so that it doesn't get returned twice.
leasedInProgress = null; leasedInProgress = null;
} }
private class OngoingOperation : IDisposable
{
private readonly OngoingOperationTracker tracker;
private readonly LeasedBindable<bool> lease;
public OngoingOperation(OngoingOperationTracker tracker, LeasedBindable<bool> lease)
{
this.tracker = tracker;
this.lease = lease;
}
public void Dispose()
{
tracker.endOperationWithKnownLease(lease);
}
}
} }
} }

View File

@ -33,6 +33,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private OverlinedHeader participantsHeader; private OverlinedHeader participantsHeader;
private GridContainer mainContent;
public PlaylistsRoomSubScreen(Room room) public PlaylistsRoomSubScreen(Room room)
{ {
Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value; Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value;
@ -44,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new GridContainer mainContent = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Content = new[] Content = new[]
@ -190,6 +192,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden } State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden }
} }
}; };
if (roomId.Value == null)
{
// A new room is being created.
// The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed.
mainContent.Hide();
settingsOverlay.State.BindValueChanged(visibility =>
{
if (visibility.NewValue == Visibility.Hidden)
mainContent.Show();
}, true);
}
} }
[Resolved] [Resolved]

View File

@ -29,7 +29,11 @@ namespace osu.Game.Screens.Play
public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; public BeatmapMetadata Metadata => PlayableBeatmap.Metadata;
public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; public ControlPointInfo ControlPointInfo
{
get => PlayableBeatmap.ControlPointInfo;
set => PlayableBeatmap.ControlPointInfo = value;
}
public List<BreakPeriod> Breaks => PlayableBeatmap.Breaks; public List<BreakPeriod> Breaks => PlayableBeatmap.Breaks;

View File

@ -53,8 +53,6 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api) private void load(OsuConfigManager config, IAPIProvider api)
{ {
streamingClient.OnNewFrames += handleIncomingFrames;
foreach (var userId in playingUsers) foreach (var userId in playingUsers)
{ {
streamingClient.WatchUser(userId); streamingClient.WatchUser(userId);
@ -90,6 +88,9 @@ namespace osu.Game.Screens.Play.HUD
playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
playingUsers.BindCollectionChanged(usersChanged); playingUsers.BindCollectionChanged(usersChanged);
// this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
streamingClient.OnNewFrames += handleIncomingFrames;
} }
private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)

View File

@ -918,15 +918,10 @@ namespace osu.Game.Screens.Select
} }
} }
protected class CarouselScrollContainer : OsuScrollContainer<DrawableCarouselItem> protected class CarouselScrollContainer : UserTrackingScrollContainer<DrawableCarouselItem>
{ {
private bool rightMouseScrollBlocked; private bool rightMouseScrollBlocked;
/// <summary>
/// Whether the last scroll event was user triggered, directly on the scroll container.
/// </summary>
public bool UserScrolling { get; private set; }
public CarouselScrollContainer() public CarouselScrollContainer()
{ {
// size is determined by the carousel itself, due to not all content necessarily being loaded. // size is determined by the carousel itself, due to not all content necessarily being loaded.
@ -936,18 +931,6 @@ namespace osu.Game.Screens.Select
Masking = false; Masking = false;
} }
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
{
UserScrolling = true;
base.OnUserScroll(value, animated, distanceDecay);
}
public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null)
{
UserScrolling = false;
base.ScrollTo(value, animated, distanceDecay);
}
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (e.Button == MouseButton.Right) if (e.Button == MouseButton.Right)

View File

@ -10,6 +10,8 @@ using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
@ -78,5 +80,9 @@ namespace osu.Game.Screens.Select
item.RequiredMods.Clear(); item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
} }
protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(isValidMod);
private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true;
} }
} }

View File

@ -251,11 +251,7 @@ namespace osu.Game.Screens.Select
Children = new Drawable[] Children = new Drawable[]
{ {
BeatmapOptions = new BeatmapOptionsOverlay(), BeatmapOptions = new BeatmapOptionsOverlay(),
ModSelect = new ModSelectOverlay ModSelect = CreateModSelectOverlay()
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
}
} }
} }
} }
@ -305,6 +301,8 @@ namespace osu.Game.Screens.Select
} }
} }
protected virtual ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay();
protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) protected virtual void ApplyFilterToCarousel(FilterCriteria criteria)
{ {
// if not the current screen, we want to get carousel in a good presentation state before displaying (resume or enter). // if not the current screen, we want to get carousel in a good presentation state before displaying (resume or enter).

View File

@ -126,6 +126,9 @@ namespace osu.Game.Users
[JsonProperty(@"follower_count")] [JsonProperty(@"follower_count")]
public int FollowerCount; public int FollowerCount;
[JsonProperty(@"mapping_follower_count")]
public int MappingFollowerCount;
[JsonProperty(@"favourite_beatmapset_count")] [JsonProperty(@"favourite_beatmapset_count")]
public int FavouriteBeatmapsetCount; public int FavouriteBeatmapsetCount;

View File

@ -1,52 +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.Collections.Generic;
using System.Linq;
namespace osu.Game.Utils
{
public static class OrderAttributeUtils
{
/// <summary>
/// Get values of an enum in order. Supports custom ordering via <see cref="OrderAttribute"/>.
/// </summary>
public static IEnumerable<T> GetValuesInOrder<T>()
{
var type = typeof(T);
if (!type.IsEnum)
throw new InvalidOperationException("T must be an enum");
IEnumerable<T> items = (T[])Enum.GetValues(type);
if (Attribute.GetCustomAttribute(type, typeof(HasOrderedElementsAttribute)) == null)
return items;
return items.OrderBy(i =>
{
if (type.GetField(i.ToString()).GetCustomAttributes(typeof(OrderAttribute), false).FirstOrDefault() is OrderAttribute attr)
return attr.Order;
throw new ArgumentException($"Not all values of {nameof(T)} have {nameof(OrderAttribute)} specified.");
});
}
}
[AttributeUsage(AttributeTargets.Field)]
public class OrderAttribute : Attribute
{
public readonly int Order;
public OrderAttribute(int order)
{
Order = order;
}
}
[AttributeUsage(AttributeTargets.Enum)]
public class HasOrderedElementsAttribute : Attribute
{
}
}

View File

@ -18,15 +18,16 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="DiffPlex" Version="1.6.3" /> <PackageReference Include="DiffPlex" Version="1.6.3" />
<PackageReference Include="Humanizer" Version="2.8.26" /> <PackageReference Include="Humanizer" Version="2.8.26" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.1.10" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.1.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="3.1.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="3.1.10" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="3.1.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.118.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="Sentry" Version="2.1.8" /> <PackageReference Include="Sentry" Version="2.1.8" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

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