Files
Ez2Lazer/osu.Game/Screens/Play/SubmittingPlayer.cs
LA 0b9f9f70d6 主要为代码质量更新
1. 匹配新版按钮控件的自动宽度写法

2. 统一Ez日志写入方向

3.移除历史修改:缓存启用mod列表,切换mod时保持通用mod开启状态

4.代码格式化、

5.修改文件名称表意,更直观
2026-03-12 19:29:55 +08:00

422 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.
#nullable disable
using System;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.LAsEzExtensions.Analysis;
using osu.Game.LAsEzExtensions.Audio;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
{
/// <summary>
/// A player instance which supports submitting scores to an online store.
/// </summary>
public abstract partial class SubmittingPlayer : Player
{
/// <summary>
/// The token to be used for the current submission. This is fetched via a request created by <see cref="CreateTokenRequest"/>.
/// </summary>
private long? token;
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private SpectatorClient spectatorClient { get; set; }
[Resolved]
private SessionStatics statics { get; set; }
[Resolved(canBeNull: true)]
[CanBeNull]
private UserStatisticsWatcher userStatisticsWatcher { get; set; }
[Resolved]
private Ez2ConfigManager ezConfig { get; set; }
[CanBeNull]
private InputAudioLatencyTracker latencyTracker;
private readonly object scoreSubmissionLock = new object();
private TaskCompletionSource<bool> scoreSubmissionSource;
protected SubmittingPlayer(PlayerConfiguration configuration = null)
: base(configuration)
{
}
[BackgroundDependencyLoader]
private void load()
{
if (DrawableRuleset == null)
{
// base load must have failed (e.g. due to an unknown mod); bail.
return;
}
AddInternal(new PlayerTouchInputDetector());
// We probably want to move this display to something more global.
// Probably using the OSD somehow.
AddInternal(new GameplayOffsetControl
{
Margin = new MarginPadding(20),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
});
// 保存配置实例并初始化延迟追踪
latencyTracker = new InputAudioLatencyTracker(ezConfig);
latencyTracker?.Initialize(ScoreProcessor);
// Ensure tracker is started for testing scenarios in SubmittingPlayer
try
{
latencyTracker?.Start();
}
catch (Exception ex)
{
Logger.Log($"InputAudioLatencyTracker failed to Start: {ex.Message}", level: LogLevel.Error);
}
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart)
{
ShouldValidatePlaybackRate = true,
};
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
handleTokenRetrieval();
}
private bool handleTokenRetrieval()
{
// Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request.
var tcs = new TaskCompletionSource<bool>();
if (Mods.Value.Any(m => !m.UserPlayable))
{
handleTokenFailure(new InvalidOperationException("Non-user playable mod selected."));
return false;
}
if (!api.IsLoggedIn)
{
handleTokenFailure(new InvalidOperationException("API is not online."));
return false;
}
var req = CreateTokenRequest();
if (req == null)
{
handleTokenFailure(new InvalidOperationException("Request could not be constructed."));
return false;
}
req.Success += r =>
{
Logger.Log($"Score submission token retrieved ({r.ID})");
token = r.ID;
tcs.SetResult(true);
};
req.Failure += ex => handleTokenFailure(ex, displayNotification: true);
api.Queue(req);
// Generally a timeout would not happen here as APIAccess will timeout first.
if (!tcs.Task.Wait(30000))
req.TriggerFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
return true;
void handleTokenFailure(Exception exception, bool displayNotification = false)
{
tcs.SetResult(false);
bool shouldExit = ShouldExitOnTokenRetrievalFailure(exception);
if (displayNotification || shouldExit)
{
string whatWillHappen = shouldExit
? "Play in this state is not permitted."
: "Your score will not be submitted.";
if (string.IsNullOrEmpty(exception.Message))
Logger.Error(exception, $"Failed to retrieve a score submission token.\n\n{whatWillHappen}");
else
{
switch (exception.Message)
{
case @"missing token header":
case @"invalid client hash":
case @"invalid verification hash":
Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important);
break;
case @"invalid or missing beatmap_hash":
Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important);
break;
case @"expired token":
Logger.Log($"Your system clock is set incorrectly. Please check your system time, date and timezone.\n\n{whatWillHappen}", level: LogLevel.Important);
break;
default:
Logger.Log($"{whatWillHappen} {exception.Message}", level: LogLevel.Important);
break;
}
}
}
if (shouldExit)
{
Schedule(() =>
{
ValidForResume = false;
this.Exit();
});
}
}
}
/// <summary>
/// Called when a token could not be retrieved for submission.
/// </summary>
/// <param name="exception">The error causing the failure.</param>
/// <returns>Whether gameplay should be immediately exited as a result. Returning false allows the gameplay session to continue. Defaults to true.</returns>
protected virtual bool ShouldExitOnTokenRetrievalFailure(Exception exception) => true;
// 重写允许在游戏过程中调整关键设置如速度修改器大于10秒后、非休息时间且未暂停时不允许调整
// public override bool AllowCriticalSettingsAdjustment
// {
// get
// {
// // General limitations to ensure players don't do anything too weird.
// // These match stable for now.
//
// // TODO: the blocking conditions should probably display a message.
// if (!IsBreakTime.Value && GameplayClockContainer.CurrentTime - GameplayClockContainer.GameplayStartTime > 10000)
// return false;
//
// if (GameplayClockContainer.IsPaused.Value)
// return false;
//
// return base.AllowCriticalSettingsAdjustment;
// }
// }
protected override async Task PrepareScoreForResultsAsync(Score score)
{
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
score.ScoreInfo.Date = DateTimeOffset.Now;
await submitScore(score).ConfigureAwait(false);
spectatorClient.EndPlaying(GameplayState);
userStatisticsWatcher?.RegisterForStatisticsUpdateAfter(score.ScoreInfo);
}
[Resolved]
private RealmAccess realm { get; set; }
protected override void StartGameplay()
{
base.StartGameplay();
// User expectation is that last played should be updated when entering the gameplay loop
// from multiplayer / playlists / solo.
realm.WriteAsync(r =>
{
var realmBeatmap = r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID);
if (realmBeatmap != null)
realmBeatmap.LastPlayed = DateTimeOffset.Now;
});
spectatorClient.BeginPlaying(token, GameplayState, Score);
}
public override bool Pause()
{
bool wasPaused = GameplayClockContainer.IsPaused.Value;
bool paused = base.Pause();
if (!wasPaused && paused)
Score.ScoreInfo.Pauses.Add((int)Math.Round(GameplayClockContainer.CurrentTime));
return paused;
}
protected override void ConcludeFailedScore(Score score)
{
base.ConcludeFailedScore(score);
submitFromFailOrQuit(score);
}
public override bool OnExiting(ScreenExitEvent e)
{
bool exiting = base.OnExiting(e);
submitFromFailOrQuit(Score);
statics.SetValue(Static.LastLocalUserScore, Score?.ScoreInfo.DeepClone());
// 生成延迟报告
latencyTracker?.GenerateLatencyReport();
return exiting;
}
private void submitFromFailOrQuit(Score score)
{
if (LoadedBeatmapSuccessfully)
{
// compare: https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Play/Player.cs#L848-L851
var scoreCopy = score.DeepClone();
Task.Run(async () =>
{
await submitScore(scoreCopy).ConfigureAwait(false);
spectatorClient.EndPlaying(GameplayState);
}).FireAndForget();
}
}
/// <summary>
/// Construct a request to be used for retrieval of the score token.
/// Can return null, at which point <see cref="ShouldExitOnTokenRetrievalFailure"/> will be fired.
/// </summary>
[CanBeNull]
protected abstract APIRequest<APIScoreToken> CreateTokenRequest();
/// <summary>
/// Construct a request to submit the score.
/// Will only be invoked if the request constructed via <see cref="CreateTokenRequest"/> was successful.
/// </summary>
/// <param name="score">The score to be submitted.</param>
/// <param name="token">The submission token.</param>
protected abstract APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token);
private Task submitScore(Score score)
{
var masterClock = GameplayClockContainer as MasterGameplayClockContainer;
if (masterClock?.PlaybackRateValid.Value != true)
{
Logger.Log("Score submission cancelled due to audio playback rate discrepancy.");
return Task.CompletedTask;
}
var accCutoffABindable = ezConfig.GetBindable<double>(Ez2Setting.AccuracyCutoffA);
var accCutoffSBindable = ezConfig.GetBindable<double>(Ez2Setting.AccuracyCutoffS);
bool hasDefaultCutoffValues = accCutoffABindable.IsDefault || accCutoffSBindable.IsDefault;
// 如果当前所选的 HitMode 不是 Lazer则强制跳过上传成绩
var hitMode = ezConfig.Get<EzEnumHitMode>(Ez2Setting.HitMode);
if (Ruleset.Value.OnlineID == 3 && (hitMode != EzEnumHitMode.Lazer || !hasDefaultCutoffValues))
{
Logger.Log($"[EzMania]Score submission blocked by custom rating settings (HitMode={hitMode}, CutoffA={accCutoffABindable.Value:0.####}, CutoffS={accCutoffSBindable.Value:0.####})."
, Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
return Task.CompletedTask;
}
// 如果任一 offsetPlus 设置非0则禁止上传成绩以防止不公平的分数提交
var offsetManiaBindable = ezConfig.GetBindable<double>(Ez2Setting.OffsetPlusMania);
var offsetNonStdBindable = ezConfig.GetBindable<double>(Ez2Setting.OffsetPlusNonMania);
if (Ruleset.Value.OnlineID == 3 && !offsetManiaBindable.IsDefault)
{
Logger.Log($"[EzMania]Score submission blocked by offset settings.", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
return Task.CompletedTask;
}
if (Ruleset.Value.OnlineID != 3 && !offsetNonStdBindable.IsDefault)
{
Logger.Log($"[EzNoMania]Score submission blocked by offset settings.", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
return Task.CompletedTask;
}
// token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
if (token == null)
{
Logger.Log("No token, skipping score submission");
return Task.CompletedTask;
}
// if the user never hit anything, this score should not be counted in any way.
if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0))
{
Logger.Log("No hits registered, skipping score submission");
return Task.CompletedTask;
}
// zero scores should also never be submitted.
if (score.ScoreInfo.TotalScore == 0)
{
Logger.Log("Zero score, skipping score submission");
return Task.CompletedTask;
}
// mind the timing of this.
// once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background,
// so all exceptional circumstances that would disallow submission must be handled above.
lock (scoreSubmissionLock)
{
if (scoreSubmissionSource != null)
return scoreSubmissionSource.Task;
scoreSubmissionSource = new TaskCompletionSource<bool>();
}
Logger.Log($"Beginning score submission (token:{token.Value})...");
var request = CreateSubmissionRequest(score, token.Value);
request.Success += s =>
{
score.ScoreInfo.OnlineID = s.ID;
score.ScoreInfo.Position = s.Position;
scoreSubmissionSource.SetResult(true);
Logger.Log($"Score submission completed! (token:{token.Value} id:{s.ID})");
};
request.Failure += e =>
{
Logger.Error(e, $"Failed to submit score (token:{token.Value}): {e.Message}");
scoreSubmissionSource.SetResult(false);
};
api.Queue(request);
return scoreSubmissionSource.Task;
}
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score)
{
AllowRetry = true,
IsLocalPlay = true,
};
}
}