mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-13 11:20:28 +00:00
1. 匹配新版按钮控件的自动宽度写法 2. 统一Ez日志写入方向 3.移除历史修改:缓存启用mod列表,切换mod时保持通用mod开启状态 4.代码格式化、 5.修改文件名称表意,更直观
422 lines
16 KiB
C#
422 lines
16 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.
|
||
|
||
#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,
|
||
};
|
||
}
|
||
}
|