mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 00:30:19 +08:00
Merge branch 'master' into better-user-tag-ui
This commit is contained in:
@@ -85,9 +85,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||
|
||||
// When hit, don't animate further. This avoids a scale being applied on a scale and looking very weird.
|
||||
return;
|
||||
}
|
||||
else
|
||||
Scale = Vector2.One;
|
||||
|
||||
Scale = Vector2.One;
|
||||
|
||||
const float move_distance = -12;
|
||||
const float scale_amount = 1.3f;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Bogus" Version="35.6.2" />
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Bogus" Version="35.6.2" />
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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 Bogus;
|
||||
using MessagePack;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Tests.OnlinePlay
|
||||
{
|
||||
[TestFixture]
|
||||
public class MultiplayerPlaylistItemTest
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Randomizer.Seed = new Random(1337);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCloneMultiplayerPlaylistItem()
|
||||
{
|
||||
var faker = new Faker<MultiplayerPlaylistItem>()
|
||||
.StrictMode(true)
|
||||
.RuleFor(o => o.ID, f => f.Random.Long())
|
||||
.RuleFor(o => o.OwnerID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash())
|
||||
.RuleFor(o => o.RulesetID, f => f.Random.Int())
|
||||
.RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.Expired, f => f.Random.Bool())
|
||||
.RuleFor(o => o.PlaylistOrder, f => f.Random.UShort())
|
||||
.RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset())
|
||||
.RuleFor(o => o.StarRating, f => f.Random.Double())
|
||||
.RuleFor(o => o.Freestyle, f => f.Random.Bool());
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
MultiplayerPlaylistItem item = faker.Generate();
|
||||
Assert.That(MessagePackSerializer.SerializeToJson(item.Clone()), Is.EqualTo(MessagePackSerializer.SerializeToJson(item)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConstructFromAPIModel()
|
||||
{
|
||||
var faker = new Faker<MultiplayerPlaylistItem>()
|
||||
.StrictMode(true)
|
||||
.RuleFor(o => o.ID, f => f.Random.Long())
|
||||
.RuleFor(o => o.OwnerID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash())
|
||||
.RuleFor(o => o.RulesetID, f => f.Random.Int())
|
||||
.RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.Expired, f => f.Random.Bool())
|
||||
.RuleFor(o => o.PlaylistOrder, f => f.Random.UShort())
|
||||
.RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset())
|
||||
.RuleFor(o => o.StarRating, f => f.Random.Double())
|
||||
.RuleFor(o => o.Freestyle, f => f.Random.Bool());
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
MultiplayerPlaylistItem initialItem = faker.Generate();
|
||||
MultiplayerPlaylistItem copiedItem = new MultiplayerPlaylistItem(new PlaylistItem(initialItem));
|
||||
Assert.That(MessagePackSerializer.SerializeToJson(copiedItem), Is.EqualTo(MessagePackSerializer.SerializeToJson(initialItem)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,21 +50,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("Set short reference score", () =>
|
||||
{
|
||||
// 50 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
List<HitEvent> hitEvents =
|
||||
[
|
||||
// 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
];
|
||||
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
hitEvents.Add(new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null));
|
||||
}
|
||||
|
||||
foreach (var ev in hitEvents)
|
||||
ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -336,6 +337,61 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserModSelectUpdatesWhenNotVisible()
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
AllowedMods = [new APIMod(new OsuModFlashlight())]
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
// 1. Open the mod select overlay and enable flashlight
|
||||
|
||||
ClickButtonWhenEnabled<UserModSelectButton>();
|
||||
AddUntilStep("mod select contents loaded", () => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
|
||||
AddStep("click flashlight panel", () =>
|
||||
{
|
||||
ModPanel panel = this.ChildrenOfType<ModPanel>().Single(p => p.Mod is OsuModFlashlight);
|
||||
InputManager.MoveMouseTo(panel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("flashlight mod enabled", () => MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
|
||||
|
||||
// 2. Close the mod select overlay, edit the playlist to disable allowed mods, and then edit it again to re-enable allowed mods.
|
||||
|
||||
AddStep("close mod select overlay", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().Hide());
|
||||
AddUntilStep("mod select overlay not present", () => !this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().IsPresent);
|
||||
AddStep("disable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0])
|
||||
{
|
||||
AllowedMods = []
|
||||
})));
|
||||
// This would normally be done as part of the above operation with an actual server.
|
||||
AddStep("disable user mods", () => MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, Array.Empty<APIMod>()));
|
||||
AddUntilStep("flashlight mod disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
|
||||
AddStep("re-enable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0])
|
||||
{
|
||||
AllowedMods = [new APIMod(new OsuModFlashlight())]
|
||||
})));
|
||||
AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
|
||||
|
||||
// 3. Open the mod select overlay, check that the flashlight mod panel is deactivated.
|
||||
|
||||
ClickButtonWhenEnabled<UserModSelectButton>();
|
||||
AddUntilStep("mod select contents loaded", () => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
|
||||
AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
|
||||
AddAssert("flashlight mod panel not activated", () => !this.ChildrenOfType<ModPanel>().Single(p => p.Mod is OsuModFlashlight).Active.Value);
|
||||
}
|
||||
|
||||
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
|
||||
{
|
||||
[Resolved(canBeNull: true)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Bogus" Version="35.6.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace osu.Game.Tournament.Models
|
||||
{
|
||||
public int ID;
|
||||
|
||||
[JsonIgnore]
|
||||
public List<string> Acronyms
|
||||
{
|
||||
get
|
||||
|
||||
@@ -40,10 +40,10 @@ namespace osu.Game.Configuration
|
||||
if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs))
|
||||
return;
|
||||
|
||||
if (newScore.HitEvents.Count < 10)
|
||||
if (newScore.HitEvents.Count < 50)
|
||||
return;
|
||||
|
||||
if (newScore.HitEvents.CalculateAverageHitError() is not double averageError)
|
||||
if (newScore.HitEvents.CalculateMedianHitError() is not double medianError)
|
||||
return;
|
||||
|
||||
// keep a sane maximum number of entries.
|
||||
@@ -51,7 +51,7 @@ namespace osu.Game.Configuration
|
||||
averageHitErrorHistory.RemoveAt(0);
|
||||
|
||||
double globalOffset = configManager.Get<double>(OsuSetting.AudioOffset);
|
||||
averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset));
|
||||
averageHitErrorHistory.Add(new DataPoint(medianError, globalOffset));
|
||||
}
|
||||
|
||||
public void ClearHistory() => averageHitErrorHistory.Clear();
|
||||
|
||||
@@ -85,6 +85,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// Retrieves the active <see cref="MultiplayerPlaylistItem"/> as determined by the room's current settings.
|
||||
/// </summary>
|
||||
[IgnoreMember]
|
||||
[JsonIgnore]
|
||||
public MultiplayerPlaylistItem CurrentPlaylistItem => Playlist.Single(item => item.ID == Settings.PlaylistItemId);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -62,11 +62,20 @@ namespace osu.Game.Online.Rooms
|
||||
[Key(11)]
|
||||
public bool Freestyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiplayerPlaylistItem"/>.
|
||||
/// </summary>
|
||||
[SerializationConstructor]
|
||||
public MultiplayerPlaylistItem()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiplayerPlaylistItem"/> from an API <see cref="PlaylistItem"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will create unique instances of the <see cref="RequiredMods"/> and <see cref="AllowedMods"/> arrays but NOT unique instances of the contained <see cref="APIMod"/>s.
|
||||
/// </remarks>
|
||||
public MultiplayerPlaylistItem(PlaylistItem item)
|
||||
{
|
||||
ID = item.ID;
|
||||
@@ -82,5 +91,19 @@ namespace osu.Game.Online.Rooms
|
||||
StarRating = item.Beatmap.StarRating;
|
||||
Freestyle = item.Freestyle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of this <see cref="MultiplayerPlaylistItem"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will create unique instances of the <see cref="RequiredMods"/> and <see cref="AllowedMods"/> arrays but NOT unique instances of the contained <see cref="APIMod"/>s.
|
||||
/// </remarks>
|
||||
public MultiplayerPlaylistItem Clone()
|
||||
{
|
||||
MultiplayerPlaylistItem clone = (MultiplayerPlaylistItem)MemberwiseClone();
|
||||
clone.RequiredMods = RequiredMods.ToArray();
|
||||
clone.AllowedMods = AllowedMods.ToArray();
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +96,14 @@ namespace osu.Game.Online.Rooms
|
||||
Beatmap = beatmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PlaylistItem"/> from a <see cref="MultiplayerPlaylistItem"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will create unique instances of the <see cref="RequiredMods"/> and <see cref="AllowedMods"/> arrays but NOT unique instances of the contained <see cref="APIMod"/>s.
|
||||
/// </remarks>
|
||||
public PlaylistItem(MultiplayerPlaylistItem item)
|
||||
: this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
|
||||
: this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating, Checksum = item.BeatmapChecksum })
|
||||
{
|
||||
ID = item.ID;
|
||||
OwnerID = item.OwnerID;
|
||||
|
||||
@@ -55,20 +55,23 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the average hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
|
||||
/// Calculates the median hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
|
||||
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
|
||||
/// </returns>
|
||||
public static double? CalculateAverageHitError(this IEnumerable<HitEvent> hitEvents)
|
||||
public static double? CalculateMedianHitError(this IEnumerable<HitEvent> hitEvents)
|
||||
{
|
||||
double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
|
||||
double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).OrderBy(x => x).ToArray();
|
||||
|
||||
if (timeOffsets.Length == 0)
|
||||
return null;
|
||||
|
||||
return timeOffsets.Average();
|
||||
int center = timeOffsets.Length / 2;
|
||||
|
||||
// Use average of the 2 central values if length is even
|
||||
return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center];
|
||||
}
|
||||
|
||||
public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -27,6 +26,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Utils;
|
||||
using Container = osu.Framework.Graphics.Containers.Container;
|
||||
|
||||
@@ -62,11 +62,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
|
||||
private Sample? sampleStart;
|
||||
|
||||
/// <summary>
|
||||
/// Any mods applied by/to the local user.
|
||||
/// </summary>
|
||||
protected readonly Bindable<IReadOnlyList<Mod>> UserMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IOverlayManager? overlayManager { get; set; }
|
||||
|
||||
@@ -245,12 +240,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
}
|
||||
};
|
||||
|
||||
LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay
|
||||
{
|
||||
SelectedItem = { BindTarget = SelectedItem },
|
||||
SelectedMods = { BindTarget = UserMods },
|
||||
IsValidMod = _ => false
|
||||
});
|
||||
LoadComponent(UserModsSelectOverlay = new MultiplayerUserModSelectOverlay());
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -258,7 +248,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
base.LoadComplete();
|
||||
|
||||
SelectedItem.BindValueChanged(_ => updateSpecifics());
|
||||
UserMods.BindValueChanged(_ => updateSpecifics());
|
||||
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics());
|
||||
|
||||
@@ -441,11 +430,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
? rulesetInstance.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray()
|
||||
: item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
// Remove any user mods that are no longer allowed.
|
||||
Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
|
||||
if (!newUserMods.SequenceEqual(UserMods.Value))
|
||||
UserMods.Value = newUserMods;
|
||||
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
int beatmapId = GetGameplayBeatmap().OnlineID;
|
||||
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId);
|
||||
@@ -456,15 +440,11 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
Ruleset.Value = GetGameplayRuleset();
|
||||
|
||||
if (allowedMods.Length > 0)
|
||||
{
|
||||
UserModsSection.Show();
|
||||
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
|
||||
}
|
||||
else
|
||||
{
|
||||
UserModsSection.Hide();
|
||||
UserModsSelectOverlay.Hide();
|
||||
UserModsSelectOverlay.IsValidMod = _ => false;
|
||||
}
|
||||
|
||||
if (item.Freestyle)
|
||||
@@ -488,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
UserStyleSection.Hide();
|
||||
}
|
||||
|
||||
protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray();
|
||||
protected virtual APIMod[] GetGameplayMods() => SelectedItem.Value!.RequiredMods;
|
||||
|
||||
protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!;
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
public partial class MultiplayerUserModSelectOverlay : UserModSelectOverlay
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
private ModSettingChangeTracker? modSettingChangeTracker;
|
||||
private ScheduledDelegate? debouncedModSettingsUpdate;
|
||||
|
||||
public MultiplayerUserModSelectOverlay()
|
||||
: base(OverlayColourScheme.Plum)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
SelectedMods.BindValueChanged(onSelectedModsChanged);
|
||||
|
||||
updateValidMods();
|
||||
}
|
||||
|
||||
private void onRoomUpdated()
|
||||
{
|
||||
// Importantly, this is not scheduled because the client must not skip intermediate server states to validate the allowed mods.
|
||||
updateValidMods();
|
||||
}
|
||||
|
||||
private void onSelectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
client.ChangeUserMods(mods.NewValue).FireAndForget();
|
||||
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
|
||||
modSettingChangeTracker.SettingChanged += _ =>
|
||||
{
|
||||
// Debounce changes to mod settings so as to not thrash the network.
|
||||
debouncedModSettingsUpdate?.Cancel();
|
||||
debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
client.ChangeUserMods(SelectedMods.Value).FireAndForget();
|
||||
}, 500);
|
||||
};
|
||||
}
|
||||
|
||||
private void updateValidMods()
|
||||
{
|
||||
if (client.Room == null || client.LocalUser == null)
|
||||
return;
|
||||
|
||||
MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem;
|
||||
Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance();
|
||||
Mod[] allowedMods = currentItem.Freestyle
|
||||
? ruleset.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, client.Room.Settings.MatchType)).ToArray()
|
||||
: currentItem.AllowedMods.Select(m => m.ToMod(ruleset)).ToArray();
|
||||
|
||||
// Update the mod panels to reflect the ones which are valid for selection.
|
||||
IsValidMod = allowedMods.Length > 0
|
||||
? m => allowedMods.Any(a => a.GetType() == m.GetType())
|
||||
: _ => false;
|
||||
|
||||
// Remove any mods that are no longer allowed.
|
||||
Mod[] newUserMods = SelectedMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
|
||||
if (!newUserMods.SequenceEqual(SelectedMods.Value))
|
||||
SelectedMods.Value = newUserMods;
|
||||
|
||||
// The active mods include the playlist item's required mods which change separately from the selected mods.
|
||||
IReadOnlyList<Mod> newActiveMods = ComputeActiveMods();
|
||||
if (!newActiveMods.SequenceEqual(ActiveMods.Value))
|
||||
ActiveMods.Value = newActiveMods;
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<Mod> ComputeActiveMods()
|
||||
{
|
||||
if (client.Room == null || client.LocalUser == null)
|
||||
return [];
|
||||
|
||||
MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem;
|
||||
Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance();
|
||||
return currentItem.RequiredMods.Select(m => m.ToMod(ruleset)).Concat(base.ComputeActiveMods()).ToArray();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
|
||||
modSettingChangeTracker?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -11,9 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
@@ -23,7 +20,6 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
@@ -64,7 +60,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
base.LoadComplete();
|
||||
|
||||
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
|
||||
UserMods.BindValueChanged(onUserModsChanged);
|
||||
|
||||
client.LoadRequested += onLoadRequested;
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
@@ -306,35 +301,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
protected override void PartRoom() => client.LeaveRoom();
|
||||
|
||||
private ModSettingChangeTracker? modSettingChangeTracker;
|
||||
private ScheduledDelegate? debouncedModSettingsUpdate;
|
||||
|
||||
private void onUserModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
client.ChangeUserMods(mods.NewValue).FireAndForget();
|
||||
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
|
||||
modSettingChangeTracker.SettingChanged += onModSettingsChanged;
|
||||
}
|
||||
|
||||
private void onModSettingsChanged(Mod mod)
|
||||
{
|
||||
// Debounce changes to mod settings so as to not thrash the network.
|
||||
debouncedModSettingsUpdate?.Cancel();
|
||||
debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
client.ChangeUserMods(UserMods.Value).FireAndForget();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private void updateBeatmapAvailability(ValueChangedEvent<BeatmapAvailability> availability)
|
||||
{
|
||||
if (client.Room == null)
|
||||
@@ -462,8 +428,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
client.LoadRequested -= onLoadRequested;
|
||||
}
|
||||
|
||||
modSettingChangeTracker?.Dispose();
|
||||
}
|
||||
|
||||
public partial class AddItemButton : PurpleRoundedButton
|
||||
|
||||
@@ -135,6 +135,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public BreakOverlay BreakOverlay;
|
||||
|
||||
private LetterboxOverlay letterboxOverlay;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the gameplay is currently in a break.
|
||||
/// </summary>
|
||||
@@ -277,6 +279,12 @@ namespace osu.Game.Screens.Play
|
||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
|
||||
GameplayClockContainer.Add(new GameplayScrollWheelHandling());
|
||||
|
||||
// needs to exist in frame stable content, but is used by underlay layers so make sure assigned early.
|
||||
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
|
||||
{
|
||||
Breaks = Beatmap.Value.Beatmap.Breaks
|
||||
};
|
||||
|
||||
// load the skinning hierarchy first.
|
||||
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
|
||||
GameplayClockContainer.Add(rulesetSkinProvider);
|
||||
@@ -292,7 +300,7 @@ namespace osu.Game.Screens.Play
|
||||
Children = new[]
|
||||
{
|
||||
// underlay and gameplay should have access to the skinning sources.
|
||||
createUnderlayComponents(),
|
||||
createUnderlayComponents(Beatmap.Value),
|
||||
createGameplayComponents(Beatmap.Value)
|
||||
}
|
||||
},
|
||||
@@ -335,10 +343,13 @@ namespace osu.Game.Screens.Play
|
||||
dependencies.CacheAs(DrawableRuleset.FrameStableClock);
|
||||
dependencies.CacheAs<IGameplayClock>(DrawableRuleset.FrameStableClock);
|
||||
|
||||
letterboxOverlay.Clock = DrawableRuleset.FrameStableClock;
|
||||
letterboxOverlay.ProcessCustomClock = false;
|
||||
|
||||
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
|
||||
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
|
||||
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
|
||||
failAnimationContainer.Add(createOverlayComponents(Beatmap.Value));
|
||||
failAnimationContainer.Add(createOverlayComponents());
|
||||
|
||||
if (!DrawableRuleset.AllowGameplayOverlays)
|
||||
{
|
||||
@@ -409,14 +420,22 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||
|
||||
private Drawable createUnderlayComponents()
|
||||
private Drawable createUnderlayComponents(WorkingBeatmap working)
|
||||
{
|
||||
var container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both },
|
||||
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
letterboxOverlay = new LetterboxOverlay
|
||||
{
|
||||
BreakTracker = breakTracker,
|
||||
Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0,
|
||||
},
|
||||
new KiaiGameplayFountains(),
|
||||
},
|
||||
};
|
||||
@@ -434,15 +453,12 @@ namespace osu.Game.Screens.Play
|
||||
ScoreProcessor,
|
||||
HealthProcessor,
|
||||
new ComboEffects(ScoreProcessor),
|
||||
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
|
||||
{
|
||||
Breaks = working.Beatmap.Breaks
|
||||
}
|
||||
breakTracker,
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
private Drawable createOverlayComponents(IWorkingBeatmap working)
|
||||
private Drawable createOverlayComponents()
|
||||
{
|
||||
var container = new Container
|
||||
{
|
||||
@@ -450,13 +466,6 @@ namespace osu.Game.Screens.Play
|
||||
Children = new[]
|
||||
{
|
||||
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
|
||||
new LetterboxOverlay
|
||||
{
|
||||
Clock = DrawableRuleset.FrameStableClock,
|
||||
ProcessCustomClock = false,
|
||||
BreakTracker = breakTracker,
|
||||
Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0,
|
||||
},
|
||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
|
||||
{
|
||||
HoldToQuit =
|
||||
|
||||
@@ -63,11 +63,11 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
[Resolved]
|
||||
private IGameplayClock? gameplayClock { get; set; }
|
||||
|
||||
private double lastPlayAverage;
|
||||
private double lastPlayMedian;
|
||||
private double lastPlayBeatmapOffset;
|
||||
private HitEventTimingDistributionGraph? lastPlayGraph;
|
||||
|
||||
private SettingsButton? useAverageButton;
|
||||
private SettingsButton? calibrateFromLastPlayButton;
|
||||
|
||||
private IDisposable? beatmapOffsetSubscription;
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
var hitEvents = score.NewValue.HitEvents;
|
||||
|
||||
if (!(hitEvents.CalculateAverageHitError() is double average))
|
||||
if (!(hitEvents.CalculateMedianHitError() is double median))
|
||||
return;
|
||||
|
||||
referenceScoreContainer.Children = new Drawable[]
|
||||
@@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
// affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event,
|
||||
// i.e. an user input that the user had to *time to the track*,
|
||||
// i.e. one that it *makes sense to use* when doing anything with timing and offsets.
|
||||
if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 10)
|
||||
if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 50)
|
||||
{
|
||||
referenceScoreContainer.AddRange(new Drawable[]
|
||||
{
|
||||
@@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
return;
|
||||
}
|
||||
|
||||
lastPlayAverage = average;
|
||||
lastPlayMedian = median;
|
||||
lastPlayBeatmapOffset = Current.Value;
|
||||
|
||||
LinkFlowContainer globalOffsetText;
|
||||
@@ -239,7 +239,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
Height = 50,
|
||||
},
|
||||
new AverageHitError(hitEvents),
|
||||
useAverageButton = new SettingsButton
|
||||
calibrateFromLastPlayButton = new SettingsButton
|
||||
{
|
||||
Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay,
|
||||
Action = () =>
|
||||
@@ -247,7 +247,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
if (Current.Disabled)
|
||||
return;
|
||||
|
||||
Current.Value = lastPlayBeatmapOffset - lastPlayAverage;
|
||||
Current.Value = lastPlayBeatmapOffset - lastPlayMedian;
|
||||
lastAppliedScore.Value = ReferenceScore.Value;
|
||||
},
|
||||
},
|
||||
@@ -281,8 +281,8 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
bool allow = allowOffsetAdjust;
|
||||
|
||||
if (useAverageButton != null)
|
||||
useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2);
|
||||
if (calibrateFromLastPlayButton != null)
|
||||
calibrateFromLastPlayButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayMedian, adjustmentSinceLastPlay, Current.Precision / 2);
|
||||
|
||||
Current.Disabled = !allow;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring;
|
||||
namespace osu.Game.Screens.Ranking.Statistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays the unstable rate statistic for a given play.
|
||||
/// Displays the average hit error statistic for a given play.
|
||||
/// </summary>
|
||||
public partial class AverageHitError : SimpleStatisticItem<double?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates and computes an <see cref="AverageHitError"/> statistic.
|
||||
/// </summary>
|
||||
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
|
||||
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the average hit error based on.</param>
|
||||
public AverageHitError(IEnumerable<HitEvent> hitEvents)
|
||||
: base("Average Hit Error")
|
||||
{
|
||||
Value = hitEvents.CalculateAverageHitError();
|
||||
Value = hitEvents.CalculateMedianHitError();
|
||||
}
|
||||
|
||||
protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}";
|
||||
|
||||
Reference in New Issue
Block a user