1
0
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:
Bartłomiej Dach
2025-04-03 11:59:16 +02:00
committed by GitHub
Unverified
19 changed files with 349 additions and 107 deletions
@@ -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
View File
@@ -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;
}
}
}
+7 -1
View File
@@ -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
+25 -16
View File
@@ -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")}";