mirror of
https://github.com/ppy/osu.git
synced 2026-06-02 06:49:54 +08:00
e3eeb761aa
- Related to https://github.com/ppy/osu/issues/37818, but of no material help to it at this point (too late for that) As noted in https://github.com/ppy/osu/pull/37845#discussion_r3297203361. Upon comparison of replays recorded by the client and by the server the affected fields are: total score without mods, and the list of user pauses. Additionally, the date of setting the score may differ - server-side it seems to be written with UTC+0 while client-side it's written using the local timezone offset. Not really interested in fixing that last issue at this time. Also included is an intentionally loud disclaimer in `LegacyScoreEncoder` to tread with caution when treating the class. Not sure it'll help, and it's a bit late for it as pretty much every single versioning primitive has been ravaged to the brink of unusability, but maybe it'll help someone in the future. This also cleans up an unnecessary nullable on `FrameHeader.Mods` (added in https://github.com/ppy/osu/pull/30137). This change can be only done if users on releases earlier than 2024.1023.0 can no longer connect to spectator server. I leave it to reviewers to determine this as I have no visibility over current spectator server configuration. Inspecting the `osu_builds` table may help confirm this. If it provokes unease, I can back this change out.
255 lines
9.0 KiB
C#
255 lines
9.0 KiB
C#
// 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.Collections.Specialized;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Testing;
|
|
using osu.Framework.Utils;
|
|
using osu.Game.Configuration;
|
|
using osu.Game.Online.API;
|
|
using osu.Game.Online.API.Requests.Responses;
|
|
using osu.Game.Online.Multiplayer;
|
|
using osu.Game.Online.Spectator;
|
|
using osu.Game.Replays.Legacy;
|
|
using osu.Game.Rulesets.Scoring;
|
|
using osu.Game.Screens.Play.HUD;
|
|
using osu.Game.Screens.Play.Leaderboards;
|
|
|
|
namespace osu.Game.Tests.Visual.Multiplayer
|
|
{
|
|
public abstract partial class MultiplayerGameplayLeaderboardTestScene : OsuTestScene
|
|
{
|
|
protected const int TOTAL_USERS = 16;
|
|
|
|
protected readonly BindableList<MultiplayerRoomUser> MultiplayerUsers = new BindableList<MultiplayerRoomUser>();
|
|
|
|
protected MultiplayerLeaderboardProvider? LeaderboardProvider { get; private set; }
|
|
|
|
protected DrawableGameplayLeaderboard? Leaderboard { get; private set; }
|
|
|
|
protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId);
|
|
|
|
protected abstract MultiplayerLeaderboardProvider CreateLeaderboardProvider();
|
|
|
|
private readonly BindableList<int> multiplayerUserIds = new BindableList<int>();
|
|
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
|
|
|
|
private OsuConfigManager config = null!;
|
|
|
|
private readonly Mock<SpectatorClient> spectatorClient = new Mock<SpectatorClient>();
|
|
private readonly Mock<MultiplayerClient> multiplayerClient = new Mock<MultiplayerClient>();
|
|
|
|
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
|
|
Dependencies.CacheAs(spectatorClient.Object);
|
|
Dependencies.CacheAs(multiplayerClient.Object);
|
|
|
|
// To emulate `MultiplayerClient.CurrentMatchPlayingUserIds` we need a bindable list of *only IDs*.
|
|
// This tracks the list of users 1:1.
|
|
MultiplayerUsers.BindCollectionChanged((_, e) =>
|
|
{
|
|
switch (e.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
Debug.Assert(e.NewItems != null);
|
|
|
|
foreach (var user in e.NewItems.OfType<MultiplayerRoomUser>())
|
|
multiplayerUserIds.Add(user.UserID);
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Remove:
|
|
Debug.Assert(e.OldItems != null);
|
|
|
|
foreach (var user in e.OldItems.OfType<MultiplayerRoomUser>())
|
|
multiplayerUserIds.Remove(user.UserID);
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Reset:
|
|
multiplayerUserIds.Clear();
|
|
break;
|
|
}
|
|
});
|
|
|
|
multiplayerClient.SetupGet(c => c.CurrentMatchPlayingUserIds)
|
|
.Returns(() => multiplayerUserIds);
|
|
|
|
spectatorClient.SetupGet(c => c.WatchedUserStates)
|
|
.Returns(() => watchedUserStates);
|
|
}
|
|
|
|
[SetUpSteps]
|
|
public virtual void SetUpSteps()
|
|
{
|
|
AddStep("reset counts", () =>
|
|
{
|
|
spectatorClient.Invocations.Clear();
|
|
lastHeaders.Clear();
|
|
});
|
|
|
|
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = new APIUser
|
|
{
|
|
Id = 1,
|
|
});
|
|
|
|
AddStep("populate users", () =>
|
|
{
|
|
MultiplayerUsers.Clear();
|
|
|
|
for (int i = 0; i < TOTAL_USERS; i++)
|
|
{
|
|
var user = CreateUser(i);
|
|
|
|
MultiplayerUsers.Add(user);
|
|
|
|
watchedUserStates[i] = new SpectatorState
|
|
{
|
|
BeatmapID = 0,
|
|
RulesetID = 0,
|
|
Mods = user.Mods,
|
|
MaximumStatistics = new Dictionary<HitResult, int>
|
|
{
|
|
{ HitResult.Perfect, 100 }
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
AddStep("create leaderboard", () =>
|
|
{
|
|
Clear(true);
|
|
|
|
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
|
|
|
LoadComponentAsync(LeaderboardProvider = CreateLeaderboardProvider(), Add);
|
|
Add(new DependencyProvidingContainer
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
CachedDependencies = [(typeof(IGameplayLeaderboardProvider), LeaderboardProvider)],
|
|
Child = Leaderboard = new DrawableGameplayLeaderboard
|
|
{
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
}
|
|
});
|
|
});
|
|
|
|
AddUntilStep("wait for load", () => Leaderboard!.IsLoaded);
|
|
|
|
AddUntilStep("check watch requests were sent", () =>
|
|
{
|
|
try
|
|
{
|
|
foreach (var user in MultiplayerUsers)
|
|
spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once);
|
|
|
|
return true;
|
|
}
|
|
catch (MockException)
|
|
{
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void TestScoreUpdates()
|
|
{
|
|
AddRepeatStep("update state", UpdateUserStatesRandomly, 100);
|
|
AddToggleStep("switch compact mode", collapsed => Leaderboard!.CollapseDuringGameplay.Value = collapsed);
|
|
}
|
|
|
|
[Test]
|
|
public void TestUserQuit()
|
|
{
|
|
AddUntilStep("mark users quit", () =>
|
|
{
|
|
if (MultiplayerUsers.Count == 0)
|
|
return true;
|
|
|
|
MultiplayerUsers.RemoveAt(0);
|
|
return false;
|
|
});
|
|
|
|
AddUntilStep("check stop watching requests were sent", () =>
|
|
{
|
|
try
|
|
{
|
|
foreach (var user in MultiplayerUsers)
|
|
spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once);
|
|
return true;
|
|
}
|
|
catch (MockException)
|
|
{
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void TestChangeScoringMode()
|
|
{
|
|
AddRepeatStep("update state", UpdateUserStatesRandomly, 5);
|
|
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
|
|
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
|
|
}
|
|
|
|
protected void UpdateUserStatesRandomly()
|
|
{
|
|
foreach (var user in MultiplayerUsers)
|
|
{
|
|
if (RNG.NextBool())
|
|
continue;
|
|
|
|
int userId = user.UserID;
|
|
|
|
if (!lastHeaders.TryGetValue(userId, out var header))
|
|
{
|
|
lastHeaders[userId] = header = new FrameHeader(0, 0, 0, 0, new Dictionary<HitResult, int>
|
|
{
|
|
[HitResult.Miss] = 0,
|
|
[HitResult.Meh] = 0,
|
|
[HitResult.Great] = 0
|
|
}, new ScoreProcessorStatistics(), DateTimeOffset.Now, [], 0, []);
|
|
}
|
|
|
|
switch (RNG.Next(0, 3))
|
|
{
|
|
case 0:
|
|
header.Combo = 0;
|
|
header.Statistics[HitResult.Miss]++;
|
|
break;
|
|
|
|
case 1:
|
|
header.Combo++;
|
|
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
|
|
header.Statistics[HitResult.Meh]++;
|
|
header.TotalScore += 50;
|
|
break;
|
|
|
|
default:
|
|
header.Combo++;
|
|
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
|
|
header.Statistics[HitResult.Great]++;
|
|
header.TotalScore += 300;
|
|
break;
|
|
}
|
|
|
|
spectatorClient.Raise(s => s.OnNewFrames -= null, userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|