1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 19:03:08 +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>
<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>
</Project>

View File

@ -24,16 +24,13 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NETCore.Targets" 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.Design" Version="2.2.6" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<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 Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -45,6 +45,11 @@ namespace osu.Game.Rulesets.Catch.Replays
float positionChange = Math.Abs(lastPosition - h.EffectiveX);
double timeAvailable = h.StartTime - lastTime;
if (timeAvailable < 0)
{
return;
}
// 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)
// 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.
// 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;
namespace osu.Game.Rulesets.Taiko.Mods
{
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.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods
@ -8,5 +9,16 @@ namespace osu.Game.Rulesets.Taiko.Mods
public class TaikoModEasy : ModEasy
{
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.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.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 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.Graphics;
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 osuTK;
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneEditorSummaryTimeline : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
[BackgroundDependencyLoader]
private void load()
{

View File

@ -17,10 +17,12 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI;
using osuTK;
using osuTK.Graphics;
@ -129,6 +131,31 @@ namespace osu.Game.Tests.Visual.Gameplay
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", () =>
{
var ruleset = new TestPoolingRuleset();
@ -192,6 +219,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void load()
{
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize);
}
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)
{
switch (original)
{
case TestKilledHitObject h:
yield return h;
break;
default:
yield return new TestHitObject
{
StartTime = original.StartTime,
Duration = 250
};
break;
}
}
}
#endregion
#region HitObject
#region HitObjects
private class TestHitObject : ConvertHitObject
private class TestHitObject : ConvertHitObject, IHasDuration
{
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
}
}

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.
using osu.Game.Overlays;
using System.Collections.Generic;
using System.Linq;
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
{
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]
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]
public void TestHide()
private void fetchFor(params BeatmapSetInfo[] beatmaps)
{
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("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
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.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
}
[Test]
@ -310,12 +310,12 @@ namespace osu.Game.Tests.Visual.Online
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
{
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.Framework.Allocation;
using osu.Game.Overlays;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Tests.Visual.Online
{
@ -16,13 +18,33 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
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]
public void TestUserCommentPill()
{
AddStep("Hide login overlay", () => login.Hide());
AddStep("Log in", logIn);
AddStep("User comment", () => addVotePill(getUserComment()));
AddAssert("Background is transparent", () => votePill.Background.Alpha == 0);
AddStep("Click", () => votePill.Click());
AddAssert("Not loading", () => !votePill.IsLoading);
}
@ -30,8 +52,10 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestRandomCommentPill()
{
AddStep("Hide login overlay", () => login.Hide());
AddStep("Log in", logIn);
AddStep("Random comment", () => addVotePill(getRandomComment()));
AddAssert("Background is visible", () => votePill.Background.Alpha == 1);
AddStep("Click", () => votePill.Click());
AddAssert("Loading", () => votePill.IsLoading);
}
@ -39,10 +63,11 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestOfflineRandomCommentPill()
{
AddStep("Hide login overlay", () => login.Hide());
AddStep("Log out", API.Logout);
AddStep("Random comment", () => addVotePill(getRandomComment()));
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");
@ -63,12 +88,22 @@ namespace osu.Game.Tests.Visual.Online
private void addVotePill(Comment comment)
{
Clear();
Add(votePill = new VotePill(comment)
pillContainer.Clear();
pillContainer.Child = votePill = new TestPill(comment)
{
Anchor = 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
@ -85,8 +83,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("move mouse to create button", () =>
{
var footer = match.ChildrenOfType<Footer>().Single();
InputManager.MoveMouseTo(footer.ChildrenOfType<OsuButton>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<PlaylistsMatchSettingsOverlay.CreateRoomButton>().Single());
});
AddStep("click", () => InputManager.Click(MouseButton.Left));

View File

@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = Array.Empty<Mod>();
Children = new Drawable[]
{
modSelect = new TestModSelectOverlay
@ -134,6 +135,8 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestExternallySetCustomizedMod()
{
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Tournament.Components;
@ -16,5 +18,23 @@ namespace osu.Game.Tournament.Tests.Screens
Add(new TourneyVideo("main") { RelativeSizeAxes = Axes.Both });
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.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
@ -23,7 +22,7 @@ namespace osu.Game.Tournament.Components
public class TournamentBeatmapPanel : CompositeDrawable
{
public readonly BeatmapInfo Beatmap;
private readonly string mods;
private readonly string mod;
private const float horizontal_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 Box flash;
public TournamentBeatmapPanel(BeatmapInfo beatmap, string mods = null)
public TournamentBeatmapPanel(BeatmapInfo beatmap, string mod = null)
{
if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
Beatmap = beatmap;
this.mods = mods;
this.mod = mod;
Width = 400;
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,
Origin = Anchor.CentreRight,
Margin = new MarginPadding(10),
Child = new Sprite
{
FillMode = FillMode.Fit,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Texture = textures.Get($"mods/{mods}"),
}
Width = 60,
RelativeSizeAxes = Axes.Y,
});
}
}

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,
Children = new Drawable[]
{
new TournamentSpriteText
{
Text = "Starting ",
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
},
new DrawableDate(match.NewValue.Date.Value)
new ScheduleMatchDate(match.NewValue.Date.Value)
{
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
{
protected override Container<Drawable> Content => content;

View File

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

View File

@ -7,7 +7,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using Microsoft.Data.Sqlite;
using osu.Framework.Development;
using osu.Framework.IO.Network;
@ -154,23 +153,34 @@ namespace osu.Game.Beatmaps
{
using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online")))
{
var found = db.QuerySingleOrDefault<CachedOnlineBeatmapLookup>(
"SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap);
db.Open();
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.BeatmapSet.Status = status;
beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id;
beatmap.OnlineBeatmapID = found.beatmap_id;
beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0);
beatmap.OnlineBeatmapID = reader.GetInt32(1);
LogForModel(set, $"Cached local retrieval for {beatmap}.");
return true;
}
}
}
}
}
catch (Exception ex)
{
LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}.");

View File

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

View File

@ -6,34 +6,11 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
namespace osu.Game.Extensions
{
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>
/// Add a continuation to be performed only after the attached task has completed.
/// </summary>

View File

@ -24,6 +24,10 @@ namespace osu.Game.Graphics.Containers
private Bindable<bool> parallaxEnabled;
private const float parallax_duration = 100;
private bool firstUpdate = true;
public ParallaxContainer()
{
RelativeSizeAxes = Axes.Both;
@ -60,17 +64,27 @@ namespace osu.Game.Graphics.Containers
input = GetContainingInputManager();
}
private bool firstUpdate = true;
protected override void Update()
{
base.Update();
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);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -9,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osu.Framework.Utils;
namespace osu.Game.Graphics.Containers
{
@ -20,6 +22,7 @@ namespace osu.Game.Graphics.Containers
where T : Drawable
{
public Bindable<T> SelectedSection { get; } = new Bindable<T>();
private Drawable lastClickedSection;
public Drawable ExpandableHeader
{
@ -36,7 +39,7 @@ namespace osu.Game.Graphics.Containers
if (value == null) return;
AddInternal(expandableHeader);
lastKnownScroll = float.NaN;
lastKnownScroll = null;
}
}
@ -52,7 +55,7 @@ namespace osu.Game.Graphics.Containers
if (value == null) return;
AddInternal(fixedHeader);
lastKnownScroll = float.NaN;
lastKnownScroll = null;
}
}
@ -71,7 +74,7 @@ namespace osu.Game.Graphics.Containers
footer.Anchor |= Anchor.y2;
footer.Origin |= Anchor.y2;
scrollContainer.Add(footer);
lastKnownScroll = float.NaN;
lastKnownScroll = null;
}
}
@ -89,21 +92,26 @@ namespace osu.Game.Graphics.Containers
headerBackgroundContainer.Add(headerBackground);
lastKnownScroll = float.NaN;
lastKnownScroll = null;
}
}
protected override Container<T> Content => scrollContentContainer;
private readonly OsuScrollContainer scrollContainer;
private readonly UserTrackingScrollContainer scrollContainer;
private readonly Container headerBackgroundContainer;
private readonly MarginPadding originalSectionsMargin;
private Drawable expandableHeader, fixedHeader, footer, headerBackground;
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()
{
@ -128,18 +136,24 @@ namespace osu.Game.Graphics.Containers
public override void Add(T drawable)
{
base.Add(drawable);
lastKnownScroll = float.NaN;
headerHeight = float.NaN;
footerHeight = float.NaN;
Debug.Assert(drawable != null);
lastKnownScroll = null;
headerHeight = null;
footerHeight = null;
}
public void ScrollTo(Drawable section) =>
scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0));
public void ScrollTo(Drawable section)
{
lastClickedSection = section;
scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - scrollContainer.DisplayableContent * scroll_y_centre - (FixedHeader?.BoundingBox.Height ?? 0));
}
public void ScrollToTop() => scrollContainer.ScrollTo(0);
[NotNull]
protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer();
protected virtual UserTrackingScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer();
[NotNull]
protected virtual FlowContainer<T> CreateScrollContentContainer() =>
@ -156,7 +170,7 @@ namespace osu.Game.Graphics.Containers
if (source == InvalidationSource.Child && (invalidation & Invalidation.DrawSize) != 0)
{
lastKnownScroll = -1;
lastKnownScroll = null;
result = true;
}
@ -167,7 +181,10 @@ namespace osu.Game.Graphics.Containers
{
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;
if (headerH != headerHeight || footerH != footerHeight)
@ -183,28 +200,39 @@ namespace osu.Game.Graphics.Containers
{
lastKnownScroll = currentScroll;
// reset last clicked section because user started scrolling themselves
if (scrollContainer.UserScrolling)
lastClickedSection = null;
if (ExpandableHeader != null && FixedHeader != null)
{
float offset = Math.Min(ExpandableHeader.LayoutSize.Y, currentScroll);
float offset = Math.Min(expandableHeaderSize, currentScroll);
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;
float scrollOffset = FixedHeader?.LayoutSize.Y ?? 0;
Func<T, float> diff = section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset;
var smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0;
if (scrollContainer.IsScrolledToEnd())
{
SelectedSection.Value = Children.LastOrDefault();
}
// 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
// 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
{
SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault()
?? Children.FirstOrDefault();
SelectedSection.Value = Children
.TakeWhile(section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollCentre <= 0)
.LastOrDefault() ?? Children.FirstOrDefault();
}
}
}
@ -214,8 +242,9 @@ namespace osu.Game.Graphics.Containers
if (!Children.Any()) return;
var newMargin = originalSectionsMargin;
newMargin.Top += headerHeight;
newMargin.Bottom += footerHeight;
newMargin.Top += (headerHeight ?? 0);
newMargin.Bottom += (footerHeight ?? 0);
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.Linq;
using Humanizer;
using MessagePack;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@ -13,16 +14,20 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Online.API
{
[MessagePackObject]
public class APIMod : IMod
{
[JsonProperty("acronym")]
[Key(0)]
public string Acronym { get; set; }
[JsonProperty("settings")]
[Key(1)]
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
[JsonConstructor]
private APIMod()
[SerializationConstructor]
public APIMod()
{
}

View File

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

View File

@ -339,7 +339,7 @@ namespace osu.Game.Online.Chat
}
/// <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>
/// <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>
@ -399,7 +399,11 @@ namespace osu.Game.Online.Chat
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;
@ -413,7 +417,7 @@ namespace osu.Game.Online.Chat
api.Queue(new LeaveChannelRequest(channel));
channel.Joined.Value = false;
}
}
});
private long lastMessageId;

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
@ -65,13 +66,19 @@ namespace osu.Game.Online.Multiplayer
if (connection != null)
return;
connection = new HubConnectionBuilder()
.WithUrl(endpoint, options =>
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
if (RuntimeInfo.SupportsJIT)
builder.AddMessagePackProtocol();
else
{
options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
})
.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; })
.Build();
// 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();
// this is kind of SILLY
// https://github.com/dotnet/aspnetcore/issues/15198

View File

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

View File

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

View File

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

View File

@ -15,7 +15,6 @@ using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -104,7 +103,7 @@ namespace osu.Game.Online.Multiplayer
if (!connected.NewValue && Room != null)
{
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.
using System;
using MessagePack;
using Newtonsoft.Json;
namespace osu.Game.Online.Rooms
@ -9,20 +10,23 @@ namespace osu.Game.Online.Rooms
/// <summary>
/// The local availability information about a certain beatmap for the client.
/// </summary>
[MessagePackObject]
public class BeatmapAvailability : IEquatable<BeatmapAvailability>
{
/// <summary>
/// The beatmap's availability state.
/// </summary>
[Key(0)]
public readonly DownloadState State;
/// <summary>
/// The beatmap's downloading progress, null when not in <see cref="DownloadState.Downloading"/> state.
/// </summary>
[Key(1)]
public readonly double? DownloadProgress;
[JsonConstructor]
private BeatmapAvailability(DownloadState state, double? downloadProgress = null)
public BeatmapAvailability(DownloadState state, double? downloadProgress = null)
{
State = state;
DownloadProgress = downloadProgress;

View File

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

View File

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

View File

@ -5,18 +5,23 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MessagePack;
using osu.Game.Online.API;
namespace osu.Game.Online.Spectator
{
[Serializable]
[MessagePackObject]
public class SpectatorState : IEquatable<SpectatorState>
{
[Key(0)]
public int? BeatmapID { get; set; }
[Key(1)]
public int? RulesetID { get; set; }
[NotNull]
[Key(2)]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
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.Extensions.DependencyInjection;
using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -116,14 +117,19 @@ namespace osu.Game.Online.Spectator
if (connection != null)
return;
connection = new HubConnectionBuilder()
.WithUrl(endpoint, options =>
{
options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
})
.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; })
.Build();
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
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)
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Utils;
using osu.Framework.Utils;
namespace osu.Game.Overlays.BeatmapListing
{

View File

@ -176,23 +176,34 @@ namespace osu.Game.Overlays
loadingLayer.Hide();
lastFetchDisplayedTime = Time.Current;
if (content == currentContent)
return;
var lastContent = currentContent;
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.
// 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.
// 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)
panelTarget.Add(content);
content.FadeIn(200, Easing.OutQuint);
content.FadeInFromZero(200, Easing.OutQuint);
currentContent = content;
}
@ -202,7 +213,7 @@ namespace osu.Game.Overlays
base.Dispose(isDisposing);
}
private class NotFoundDrawable : CompositeDrawable
public class NotFoundDrawable : CompositeDrawable
{
public NotFoundDrawable()
{

View File

@ -1,25 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
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; }
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
{
Current = Ruleset
Current = ruleset
};
protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle();
private class BeatmapHeaderTitle : OverlayTitle
{
public BeatmapHeaderTitle()

View File

@ -3,12 +3,10 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
@ -18,18 +16,21 @@ using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
public class Header : BeatmapDownloadTrackingComposite
public class BeatmapSetHeaderContent : BeatmapDownloadTrackingComposite
{
private const float transition_duration = 200;
private const float buttons_height = 45;
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 Box coverGradient;
private readonly OsuSpriteText title, artist;
@ -38,52 +39,23 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly FillFlowContainer downloadButtonsContainer;
private readonly BeatmapAvailability beatmapAvailability;
private readonly BeatmapSetOnlineStatusPill onlineStatusPill;
public Details Details;
public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
private readonly FavouriteButton favouriteButton;
private readonly FillFlowContainer fadeContent;
private readonly LoadingSpinner loading;
[Resolved]
private IAPIProvider api { get; set; }
public BeatmapRulesetSelector RulesetSelector => beatmapSetHeader.RulesetSelector;
public readonly BeatmapPicker Picker;
[Resolved]
private BeatmapRulesetSelector rulesetSelector { get; set; }
private readonly FavouriteButton favouriteButton;
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()
public BeatmapSetHeaderContent()
{
ExternalLinkButton externalLink;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
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
InternalChild = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -217,8 +189,6 @@ namespace osu.Game.Overlays.BeatmapSet
Details = new Details(),
},
},
},
},
}
};
@ -239,7 +209,7 @@ namespace osu.Game.Overlays.BeatmapSet
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;
if (setInfo.NewValue == null)

View File

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

View File

@ -19,15 +19,12 @@ using osuTK;
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 Y_PADDING = 25;
public const float RIGHT_WIDTH = 275;
//todo: should be an OverlayHeader? or maybe not?
protected new readonly Header Header;
[Resolved]
private RulesetStore rulesets { get; set; }
@ -39,7 +36,7 @@ namespace osu.Game.Overlays
private readonly Box background;
public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue, null)
: base(OverlayColourScheme.Blue, new BeatmapSetHeader())
{
OverlayScrollContainer scroll;
Info info;
@ -72,14 +69,14 @@ namespace osu.Game.Overlays
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header = new Header(),
Header,
info = new Info()
}
},
},
new ScoresContainer
{
Beatmap = { BindTarget = Header.Picker.Beatmap }
Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
},
comments = new CommentsSection()
},
@ -91,7 +88,7 @@ namespace osu.Game.Overlays
info.BeatmapSet.BindTo(beatmapSet);
comments.BeatmapSet.BindTo(beatmapSet);
Header.Picker.Beatmap.ValueChanged += b =>
Header.HeaderContent.Picker.Beatmap.ValueChanged += b =>
{
info.Beatmap = b.NewValue;
@ -125,7 +122,7 @@ namespace osu.Game.Overlays
req.Success += res =>
{
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);

View File

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

View File

@ -33,11 +33,16 @@ namespace osu.Game.Overlays.Comments
[Resolved]
private IAPIProvider api { get; set; }
[Resolved(canBeNull: true)]
private LoginOverlay login { get; set; }
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
protected Box Background { get; private set; }
private readonly Comment comment;
private Box background;
private Box hoverLayer;
private CircularContainer borderContainer;
private SpriteText sideNumber;
@ -62,8 +67,12 @@ namespace osu.Game.Overlays.Comments
AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight;
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;
Background.Alpha = ownComment ? 0 : 1;
}
protected override void LoadComplete()
@ -71,12 +80,18 @@ namespace osu.Game.Overlays.Comments
base.LoadComplete();
isVoted.Value = comment.IsVoted;
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);
}
private void onAction()
{
if (!api.IsLoggedIn)
{
login?.Show();
return;
}
request = new CommentVoteRequest(comment.Id, isVoted.Value ? CommentVoteAction.UnVote : CommentVoteAction.Vote);
request.Success += onSuccess;
api.Queue(request);
@ -102,7 +117,7 @@ namespace osu.Game.Overlays.Comments
Masking = true,
Children = new Drawable[]
{
background = new Box
Background = new Box
{
RelativeSizeAxes = Axes.Both
},

View File

@ -236,13 +236,13 @@ namespace osu.Game.Overlays.Mods
{
iconsContainer.AddRange(new[]
{
backgroundIcon = new PassThroughTooltipModIcon(Mods[1])
backgroundIcon = new ModIcon(Mods[1], false)
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Position = new Vector2(1.5f),
},
foregroundIcon = new PassThroughTooltipModIcon(Mods[0])
foregroundIcon = new ModIcon(Mods[0], false)
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
@ -252,7 +252,7 @@ namespace osu.Game.Overlays.Mods
}
else
{
iconsContainer.Add(foregroundIcon = new PassThroughTooltipModIcon(Mod)
iconsContainer.Add(foregroundIcon = new ModIcon(Mod, false)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
@ -297,15 +297,5 @@ namespace osu.Game.Overlays.Mods
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
{
private readonly Func<Mod, bool> isValidMod;
public const float HEIGHT = 510;
protected readonly TriangleButton DeselectAllButton;
@ -60,8 +61,10 @@ namespace osu.Game.Overlays.Mods
private SampleChannel sampleOn, sampleOff;
public ModSelectOverlay()
public ModSelectOverlay(Func<Mod, bool> isValidMod = null)
{
this.isValidMod = isValidMod ?? (m => true);
Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2");
Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2");
Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774");
@ -213,9 +216,9 @@ namespace osu.Game.Overlays.Mods
},
new Drawable[]
{
// Footer
new Container
{
Name = "Footer content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
@ -234,10 +237,9 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomCentre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
RelativePositionAxes = Axes.X,
Width = content_width,
Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2),
LayoutDuration = 100,
LayoutEasing = Easing.OutQuint,
Padding = new MarginPadding
{
Vertical = 15,
@ -351,7 +353,7 @@ namespace osu.Game.Overlays.Mods
{
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);
foreach (var section in ModSectionsContainer.Children)
@ -403,7 +405,7 @@ namespace osu.Game.Overlays.Mods
if (mods.NewValue == null) return;
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)

View File

@ -17,9 +17,9 @@ using osuTK.Graphics;
namespace osu.Game.Overlays
{
/// <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>
public class OverlayScrollContainer : OsuScrollContainer
public class OverlayScrollContainer : UserTrackingScrollContainer
{
/// <summary>
/// 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),
Children = new Drawable[]
{
new AddFriendButton
new FollowersButton
{
User = { BindTarget = User }
},
new MappingSubscribersButton
{
RelativeSizeAxes = Axes.Y,
User = { BindTarget = User }
},
new MessageUserButton
@ -69,7 +72,6 @@ namespace osu.Game.Overlays.Profile.Header
Width = UserProfileOverlay.CONTENT_X_MARGIN,
Child = new ExpandDetailsButton
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
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()
{
Content.Alpha = 0;
RelativeSizeAxes = Axes.Y;
Child = new SpriteIcon
{

View File

@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
protected ProfileHeaderButton()
{
AutoSizeAxes = Axes.X;
Height = 40;
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 int firstNonDefaultSkinIndex
{
get
{
var index = skinItems.FindIndex(s => s.ID > 0);
if (index < 0)
index = skinItems.Count;
return index;
}
}
[Resolved]
private SkinManager skins { get; set; }
@ -96,7 +108,7 @@ namespace osu.Game.Overlays.Settings.Sections
if (skinDropdown.Items.All(s => s.ID != configBindable.Value))
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 =>
{
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()
{
skinItems = skins.GetAllUsableSkins();
// insert after lazer built-in skins
int firstNonDefault = skinItems.FindIndex(s => s.ID > 0);
if (firstNonDefault < 0)
firstNonDefault = skinItems.Count;
skinItems.Insert(firstNonDefault, random_skin_info);
skinItems.Insert(firstNonDefaultSkinIndex, random_skin_info);
sortUserSkins(skinItems);
skinDropdown.Items = skinItems;
}
private void itemUpdated(ValueChangedEvent<WeakReference<SkinInfo>> weakItem)
{
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)
@ -135,6 +165,13 @@ namespace osu.Game.Overlays.Settings.Sections
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>
{
protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl();

View File

@ -202,7 +202,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both;
}
protected override OsuScrollContainer CreateScrollContainer() => new OverlayScrollContainer();
protected override UserTrackingScrollContainer CreateScrollContainer() => new OverlayScrollContainer();
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.
// See the LICENCE file in the repository root for full licence text.
using MessagePack;
using Newtonsoft.Json;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Replays.Legacy
{
[MessagePackObject]
public class LegacyReplayFrame : ReplayFrame
{
[JsonIgnore]
[IgnoreMember]
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
[Key(1)]
public float? MouseX;
[Key(2)]
public float? MouseY;
[JsonIgnore]
[IgnoreMember]
public bool MouseLeft => MouseLeft1 || MouseLeft2;
[JsonIgnore]
[IgnoreMember]
public bool MouseRight => MouseRight1 || MouseRight2;
[JsonIgnore]
[IgnoreMember]
public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1);
[JsonIgnore]
[IgnoreMember]
public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1);
[JsonIgnore]
[IgnoreMember]
public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2);
[JsonIgnore]
[IgnoreMember]
public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2);
[Key(3)]
public 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);
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;
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.
// See the LICENCE file in the repository root for full licence text.
using MessagePack;
namespace osu.Game.Rulesets.Replays
{
[MessagePackObject]
public class ReplayFrame
{
[Key(0)]
public double Time;
public ReplayFrame()

View File

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

View File

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

View File

@ -124,9 +124,11 @@ namespace osu.Game.Rulesets.UI
Debug.Assert(drawableMap.ContainsKey(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.OnRevertResult -= onRevertResult;
drawable.OnKilled();
drawableMap.Remove(entry);

View File

@ -16,6 +16,9 @@ using osu.Framework.Bindables;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// Display the specified mod at a fixed size.
/// </summary>
public class ModIcon : Container, IHasTooltip
{
public readonly BindableBool Selected = new BindableBool();
@ -28,9 +31,10 @@ namespace osu.Game.Rulesets.UI
private readonly ModType type;
public virtual string TooltipText => mod.IconTooltip;
public virtual string TooltipText => showTooltip ? mod.IconTooltip : null;
private Mod mod;
private readonly bool showTooltip;
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.showTooltip = showTooltip;
type = mod.Type;

View File

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

View File

@ -4,7 +4,6 @@
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
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>();
protected override void LoadBeatmap(WorkingBeatmap beatmap)
protected override void LoadBeatmap(EditorBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
controlPointGroups.UnbindAll();
controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) =>
{
switch (args.Action)

View File

@ -2,15 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osuTK;
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 = Schedule(() =>
{
if (Beatmap.Value == null)
return;
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;
}
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).
}

View File

@ -21,7 +21,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary>
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>();
@ -33,10 +36,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
AddInternal(this.content = content ?? new Container<T> { RelativeSizeAxes = Axes.Both });
Beatmap.ValueChanged += b =>
beatmap.ValueChanged += b =>
{
updateRelativeChildSize();
LoadBeatmap(b.NewValue);
};
Track.ValueChanged += _ => updateRelativeChildSize();
@ -45,24 +47,26 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock)
{
Beatmap.BindTo(beatmap);
this.beatmap.BindTo(beatmap);
LoadBeatmap(EditorBeatmap);
Track.BindTo(clock.Track);
}
private void updateRelativeChildSize()
{
// 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;
Schedule(updateRelativeChildSize);
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();
}

View File

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

View File

@ -155,12 +155,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
seekTrackToCurrent();
else if (!editorClock.IsRunning)
{
// The track isn't running. There are two cases we have to be wary of:
// 1) The user flick-drags on this timeline: We want the track to follow us
// 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
// The track isn't running. There are three cases we have to be wary of:
// 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; 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
if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime)
// 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
// Checking IsSeeking covers the third case, where the transform may not have been applied yet.
if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime && !editorClock.IsSeeking)
seekTrackToCurrent();
else
scrollToTrackTime();

View File

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

View File

@ -131,6 +131,10 @@ namespace osu.Game.Screens.Edit
try
{
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)
{

View File

@ -74,7 +74,11 @@ namespace osu.Game.Screens.Edit
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;

View File

@ -35,6 +35,11 @@ namespace osu.Game.Screens.Edit
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)
: this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor)
{
@ -111,7 +116,7 @@ namespace osu.Game.Screens.Edit
if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
{
SeekTo(seekTime);
SeekSmoothlyTo(seekTime);
return;
}
@ -145,11 +150,11 @@ namespace osu.Game.Screens.Edit
// Ensure the sought point is within the boundaries
seekTime = Math.Clamp(seekTime, 0, TrackLength);
SeekTo(seekTime);
SeekSmoothlyTo(seekTime);
}
/// <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>
public double CurrentTimeAccurate =>
Transforms.OfType<TransformSeek>().FirstOrDefault()?.EndValue ?? CurrentTime;
@ -176,12 +181,29 @@ namespace osu.Game.Screens.Edit
public bool Seek(double position)
{
seekingOrStopped.Value = true;
seekingOrStopped.Value = IsSeeking = true;
ClearTransforms();
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();
double IAdjustableClock.Rate
@ -229,6 +251,8 @@ namespace osu.Game.Screens.Edit
{
if (seekingOrStopped.Value)
{
IsSeeking &= Transforms.Any();
if (track.Value?.IsRunning != true)
{
// 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.
// 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.
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)
=> this.TransformTo(this.PopulateTransform(new TransformSeek(), seek, duration, easing));

View File

@ -206,7 +206,7 @@ namespace osu.Game.Screens.Edit.Timing
Action = () =>
{
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}" });
}
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.Logging;
using osu.Framework.Screens;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@ -23,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.OnResuming(last);
if (client.Room != null)
client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true);
client.ChangeState(MultiplayerUserState.Idle);
}
protected override void UpdatePollingRate(bool isIdle)

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
@ -109,5 +110,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
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.Containers;
using osu.Framework.Screens;
using osu.Game.Extensions;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
@ -44,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[CanBeNull]
private IDisposable readyClickOperation;
private GridContainer mainContent;
public MultiplayerMatchSubScreen(Room room)
{
Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
@ -55,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
InternalChildren = new Drawable[]
{
new GridContainer
mainContent = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
@ -178,6 +179,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
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()
@ -222,7 +236,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// accessing Exception here silences any potential errors from the antecedent task
if (t.Exception != null)
{
t.CatchUnobservedExceptions(true); // will run immediately.
// gameplay was not started due to an exception; unblock button.
endOperation();
}
@ -233,11 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
client.ToggleReady()
.ContinueWith(t =>
{
t.CatchUnobservedExceptions(true); // will run immediately.
endOperation();
});
.ContinueWith(t => endOperation());
void endOperation()
{

View File

@ -9,7 +9,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
using osu.Game.Extensions;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
@ -69,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
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.
// 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.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -176,7 +175,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (Room.Host?.UserID != api.LocalUser.Value.Id)
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.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -43,17 +42,46 @@ namespace osu.Game.Screens.OnlinePlay
leasedInProgress = inProgress.BeginLease(true);
leasedInProgress.Value = true;
// for extra safety, marshal the end of operation back to the update thread if necessary.
return new InvokeOnDisposal(() => Scheduler.Add(endOperation, false));
return new OngoingOperation(this, leasedInProgress);
}
private void endOperation()
private void endOperationWithKnownLease(LeasedBindable<bool> lease)
{
if (leasedInProgress == null)
throw new InvalidOperationException("Cannot end operation multiple times.");
if (lease != leasedInProgress)
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;
}
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 GridContainer mainContent;
public PlaylistsRoomSubScreen(Room room)
{
Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value;
@ -44,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
InternalChildren = new Drawable[]
{
new GridContainer
mainContent = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
@ -190,6 +192,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
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]

View File

@ -29,7 +29,11 @@ namespace osu.Game.Screens.Play
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;

View File

@ -53,8 +53,6 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
streamingClient.OnNewFrames += handleIncomingFrames;
foreach (var userId in playingUsers)
{
streamingClient.WatchUser(userId);
@ -90,6 +88,9 @@ namespace osu.Game.Screens.Play.HUD
playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
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)

View File

@ -918,15 +918,10 @@ namespace osu.Game.Screens.Select
}
}
protected class CarouselScrollContainer : OsuScrollContainer<DrawableCarouselItem>
protected class CarouselScrollContainer : UserTrackingScrollContainer<DrawableCarouselItem>
{
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()
{
// 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;
}
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)
{
if (e.Button == MouseButton.Right)

View File

@ -10,6 +10,8 @@ using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
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.Components;
@ -78,5 +80,9 @@ namespace osu.Game.Screens.Select
item.RequiredMods.Clear();
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[]
{
BeatmapOptions = new BeatmapOptionsOverlay(),
ModSelect = new ModSelectOverlay
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
}
ModSelect = CreateModSelectOverlay()
}
}
}
@ -305,6 +301,8 @@ namespace osu.Game.Screens.Select
}
}
protected virtual ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay();
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).

View File

@ -126,6 +126,9 @@ namespace osu.Game.Users
[JsonProperty(@"follower_count")]
public int FollowerCount;
[JsonProperty(@"mapping_follower_count")]
public int MappingFollowerCount;
[JsonProperty(@"favourite_beatmapset_count")]
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>
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="DiffPlex" Version="1.6.3" />
<PackageReference Include="Humanizer" Version="2.8.26" />
<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.EntityFrameworkCore.Sqlite" 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="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="Sentry" Version="2.1.8" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<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" />
</ItemGroup>
<!-- 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.Core" Version="2.2.6" />
<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="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />