osu/osu.Game/Screens/Play/SoloSpectatorScreen.cs
Bartłomiej Dach 4b349ba387
Use cache for beatmap lookups on spectate screen
@peppy noticed recently that attempting to spectate just a few users was
very likely to end up in requests very quickly being rejected with code
429 ("Too Many Requests").

I'm somewhat certain that the reason for that is that a significant
number of players is wont to retry a lot in quick succession. That means
that spectator server is going to note a lot of gameplay start and end
messages in quick succession, too. And as it turns out, every gameplay
start would end up triggering a new beatmap set fetch request:

	ccf1acce56/osu.Game/Screens/Spectate/SpectatorScreen.cs (L131-L134)
	ccf1acce56/osu.Game/Screens/Play/SoloSpectatorScreen.cs (L168-L172)
	ccf1acce56/osu.Game/Screens/Play/SoloSpectatorScreen.cs (L243-L256)

To attempt to curtail that, use the beatmap cache instead, which should
prevent these unnecessary requests from firing in the first place,
therefore reducing the chance of the client getting throttled.

This technically means that a different endpoint is used to fetch the
data (`GET /beatmaps/?ids[]=` rather than `GET
/beatmapsets/lookup?beatmap_id={id}`), but docs claim that both should
return the same data, and it looks to work fine in practice.
2024-09-23 12:15:19 +02:00

281 lines
10 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Spectate;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.Play
{
[Cached(typeof(IPreviewTrackOwner))]
public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner
{
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; } = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[Resolved]
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private Container beatmapPanelContainer = null!;
private RoundedButton watchButton = null!;
private SettingsCheckbox automaticDownload = null!;
private readonly APIUser targetUser;
/// <summary>
/// The player's immediate online gameplay state.
/// This doesn't always reflect the gameplay state being watched.
/// </summary>
private SpectatorGameplayState? immediateSpectatorGameplayState;
private ScheduledDelegate? beatmapFetchCallback;
private APIBeatmapSet? beatmapSet;
public SoloSpectatorScreen(APIUser targetUser)
: base(targetUser.Id)
{
this.targetUser = targetUser;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
InternalChild = new Container
{
Masking = true,
CornerRadius = 20,
AutoSizeAxes = Axes.Both,
AutoSizeDuration = 500,
AutoSizeEasing = Easing.OutQuint,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background5,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Margin = new MarginPadding(20),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(15),
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Spectator Mode",
Font = OsuFont.Default.With(size: 30),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(15),
Children = new Drawable[]
{
new UserGridPanel(targetUser)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 145,
Width = 290,
},
new SpriteIcon
{
Size = new Vector2(40),
Icon = FontAwesome.Solid.ArrowRight,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
beatmapPanelContainer = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
automaticDownload = new SettingsCheckbox
{
LabelText = OnlineSettingsStrings.AutomaticallyDownloadMissingBeatmaps,
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
watchButton = new PurpleRoundedButton
{
Text = "Start Watching",
Width = 250,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = () => scheduleStart(immediateSpectatorGameplayState),
Enabled = { Value = false }
}
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload());
}
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) => Schedule(() =>
{
clearDisplay();
showBeatmapPanel(spectatorState);
});
protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => Schedule(() =>
{
immediateSpectatorGameplayState = spectatorGameplayState;
watchButton.Enabled.Value = true;
scheduleStart(spectatorGameplayState);
});
protected override void FailGameplay(int userId)
{
if (this.GetChildScreen() is SpectatorPlayerLoader loader)
{
if (loader.GetChildScreen() is SpectatorPlayer player)
{
player.AllowFail();
resetStartState();
}
else
QuitGameplay(userId);
}
}
protected override void QuitGameplay(int userId)
{
// Importantly, don't schedule this call, as a child screen may be present (and will cause the schedule to not be run as expected).
this.MakeCurrent();
resetStartState();
}
private void resetStartState() => Schedule(() =>
{
scheduledStart?.Cancel();
immediateSpectatorGameplayState = null;
clearDisplay();
});
private void clearDisplay()
{
watchButton.Enabled.Value = false;
beatmapFetchCallback?.Cancel();
beatmapPanelContainer.Clear();
previewTrackManager.StopAnyPlaying(this);
}
private ScheduledDelegate? scheduledStart;
private void scheduleStart(SpectatorGameplayState? spectatorGameplayState)
{
Debug.Assert(spectatorGameplayState != null);
// This function may be called multiple times in quick succession once the screen becomes current again.
scheduledStart?.Cancel();
scheduledStart = Schedule(() =>
{
if (this.IsCurrentScreen())
start();
else
scheduleStart(spectatorGameplayState);
});
void start()
{
Beatmap.Value = spectatorGameplayState.Beatmap;
Ruleset.Value = spectatorGameplayState.Ruleset.RulesetInfo;
this.Push(new SpectatorPlayerLoader(spectatorGameplayState.Score, () => new SoloSpectatorPlayer(spectatorGameplayState.Score)));
}
}
private void showBeatmapPanel(SpectatorState state)
{
Debug.Assert(state.BeatmapID != null);
beatmapLookupCache.GetBeatmapAsync(state.BeatmapID.Value).ContinueWith(t => beatmapFetchCallback = Schedule(() =>
{
var beatmap = t.GetResultSafely();
if (beatmap?.BeatmapSet == null)
return;
beatmapSet = beatmap.BeatmapSet;
beatmapPanelContainer.Child = new BeatmapCardNormal(beatmapSet, allowExpansion: false);
checkForAutomaticDownload();
}));
}
private void checkForAutomaticDownload()
{
if (beatmapSet == null)
return;
if (!automaticDownload.Current.Value)
return;
if (beatmaps.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID }))
return;
beatmapDownloader.Download(beatmapSet);
}
public override bool OnExiting(ScreenExitEvent e)
{
previewTrackManager.StopAnyPlaying(this);
return base.OnExiting(e);
}
}
}