Skip to content

Fix score processor no longer applying results when failing in multiplayer match #26751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public void TestSwellDoesNotFail()
Player.ScoreProcessor.NewJudgement += _ => judged = true;
});
AddUntilStep("swell judged", () => judged);
AddAssert("not failed", () => !Player.GameplayState.HasFailed);
AddAssert("not failed", () => !Player.GameplayState.ShownFailAnimation);
}
}
}
2 changes: 1 addition & 1 deletion osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void TestOsuWithoutRedTint()

protected override void AddCheckSteps()
{
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);
AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible);

// The pause screen and fail animation both ramp frequency.
Expand Down
4 changes: 2 additions & 2 deletions osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ protected override Player CreatePlayer(Ruleset ruleset)
protected override void AddCheckSteps()
{
AddUntilStep("player is playing", () => Player.LocalUserPlaying.Value);
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);
AddAssert("player is not playing", () => !Player.LocalUserPlaying.Value);
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllEntries.Count(e => e.Judged) > 1);
AddAssert("total number of results == 1", () =>
{
var score = new ScoreInfo { Ruleset = Ruleset.Value };
Expand Down
12 changes: 6 additions & 6 deletions osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public void TestExitSoonAfterResumeSucceeds()
[Test]
public void TestPauseAfterFail()
{
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);
AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible);

confirmClockRunning(false);
Expand All @@ -240,7 +240,7 @@ public void TestPauseAfterFail()
[Test]
public void TestExitFromFailedGameplayAfterFailAnimation()
{
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);
AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible);

confirmClockRunning(false);
Expand All @@ -252,7 +252,7 @@ public void TestExitFromFailedGameplayAfterFailAnimation()
[Test]
public void TestExitFromFailedGameplayDuringFailAnimation()
{
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);

// will finish the fail animation and show the fail/pause screen.
pauseViaBackAction();
Expand All @@ -266,7 +266,7 @@ public void TestExitFromFailedGameplayDuringFailAnimation()
[Test]
public void TestQuickRetryFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);
AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType<HotkeyRetryOverlay>().First().Action?.Invoke());

confirmExited();
Expand All @@ -275,7 +275,7 @@ public void TestQuickRetryFromFailedGameplay()
[Test]
public void TestQuickExitFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);
exitViaQuickExitAction();

confirmExited();
Expand Down Expand Up @@ -380,7 +380,7 @@ private void confirmPaused()
{
confirmClockRunning(false);
confirmNotExited();
AddAssert("player not failed", () => !Player.GameplayState.HasFailed);
AddAssert("player not failed", () => !Player.GameplayState.ShownFailAnimation);
AddAssert("pause overlay shown", () => Player.PauseOverlayVisible);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public void TestNoSubmissionOnEmptyFail()

AddUntilStep("wait for token request", () => Player.TokenCreationRequested);

AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);
AddStep("exit", () => Player.Exit());

AddAssert("ensure no submission", () => Player.SubmittedScore == null);
Expand All @@ -188,7 +188,7 @@ public void TestSubmissionOnFail()

addFakeHit();

AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);

AddUntilStep("wait for submission", () => Player.SubmittedScore != null);
AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false);
Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ public void TestFailedStateDuringPlay()
AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed));
AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed);

AddUntilStep("wait for player to fail", () => player.GameplayState.HasFailed);
AddUntilStep("wait for player to fail", () => player.GameplayState.ShownFailAnimation);

start();
sendFrames();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public void TestOutroEndsDuringFailAnimation()
AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600);
});

AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
Expand All @@ -116,7 +116,7 @@ public void TestSaveFailedReplayWithStoryboardEndedDoesNotProgress()
AddStep("set storyboard duration to 0s", () => currentStoryboardDuration = 0);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => Player.GameplayState.ShownFailAnimation);

AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
AddUntilStep("wait for button clickable", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().Enabled.Value);
Expand Down
4 changes: 2 additions & 2 deletions osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void TestMaximumAchievableAccuracy() =>
Position = new Vector2(i * 50)
}).Cast<HitObject>().ToList()
},
PassCondition = () => Player.GameplayState.HasFailed && Player.ScoreProcessor.JudgedHits >= 3
PassCondition = () => Player.GameplayState.ShownFailAnimation && Player.ScoreProcessor.JudgedHits >= 3
});

[Test]
Expand All @@ -64,7 +64,7 @@ public void TestStandardAccuracy() =>
Position = new Vector2(i * 50)
}).Cast<HitObject>().ToList()
},
PassCondition = () => Player.GameplayState.HasFailed && Player.ScoreProcessor.JudgedHits >= 1
PassCondition = () => Player.GameplayState.ShownFailAnimation && Player.ScoreProcessor.JudgedHits >= 1
});
}
}
40 changes: 31 additions & 9 deletions osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@

#nullable disable

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;

Expand All @@ -19,14 +24,37 @@ public partial class TestSceneMultiplayerPlayer : MultiplayerTestScene
{
private MultiplayerPlayer player;

[SetUpSteps]
public override void SetUpSteps()
[Test]
public void TestGameplay()
{
setup();

AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
}

[Test]
public void TestFail()
{
base.SetUpSteps();
setup(() => new[] { new OsuModAutopilot() });

AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
AddStep("set health zero", () => player.ChildrenOfType<HealthProcessor>().Single().Health.Value = 0);
AddUntilStep("wait for fail", () => player.ChildrenOfType<HealthProcessor>().Single().HasFailed);
AddAssert("fail animation not shown", () => !player.GameplayState.ShownFailAnimation);

// ensure that even after reaching a failed state, score processor keeps accounting for new hit results.
// the testing method used here (autopilot + hold key) is sort-of dodgy, but works enough.
AddAssert("score is zero", () => player.GameplayState.ScoreProcessor.TotalScore.Value == 0);
AddStep("hold key", () => player.ChildrenOfType<OsuInputManager.RulesetKeyBindingContainer>().First().TriggerPressed(OsuAction.LeftButton));
AddUntilStep("score changed", () => player.GameplayState.ScoreProcessor.TotalScore.Value > 0);
}

private void setup(Func<IReadOnlyList<Mod>> mods = null)
{
AddStep("set beatmap", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
SelectedMods.Value = mods?.Invoke() ?? Array.Empty<Mod>();
});

AddStep("Start track playing", () =>
Expand All @@ -52,11 +80,5 @@ public override void SetUpSteps()
AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value);
AddAssert("gameplay clock is running", () => player.ChildrenOfType<GameplayClockContainer>().Single().IsRunning);
}

[Test]
public void TestGameplay()
{
AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ public void TestSongContinuesAfterExitPlayer(bool withUserPause)
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});

AddUntilStep("wait for fail", () => player.GameplayState.HasFailed);
AddUntilStep("wait for fail", () => player.GameplayState.ShownFailAnimation);

AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime);
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/Online/Spectator/SpectatorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public void EndPlaying(GameplayState state)

if (state.HasPassed)
currentState.State = SpectatedUserState.Passed;
else if (state.HasFailed)
else if (state.ShownFailAnimation)
currentState.State = SpectatedUserState.Failed;
else
currentState.State = SpectatedUserState.Quit;
Expand Down
6 changes: 0 additions & 6 deletions osu.Game/Rulesets/Scoring/ScoreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,6 @@ protected sealed override void ApplyResultInternal(JudgementResult result)
result.ComboAtJudgement = Combo.Value;
result.HighestComboAtJudgement = HighestCombo.Value;

if (result.FailedAtJudgement)
return;

ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) + 1;

if (result.Type.IncreasesCombo())
Expand Down Expand Up @@ -267,9 +264,6 @@ protected sealed override void RevertResultInternal(JudgementResult result)
Combo.Value = result.ComboAtJudgement;
HighestCombo.Value = result.HighestComboAtJudgement;

if (result.FailedAtJudgement)
return;

ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) - 1;

if (result.Judgement.MaxResult.AffectsAccuracy())
Expand Down
19 changes: 19 additions & 0 deletions osu.Game/Rulesets/UI/Playfield.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ public IEnumerable<DrawableHitObject> AllHitObjects
}
}

/// <summary>
/// All the <see cref="HitObjectLifetimeEntry"/>s contained in this <see cref="Playfield"/> and all <see cref="NestedPlayfields"/>.
/// </summary>
public IEnumerable<HitObjectLifetimeEntry> AllEntries
{
get
{
if (HitObjectContainer == null)
return Enumerable.Empty<HitObjectLifetimeEntry>();

var enumerable = HitObjectContainer.Entries;

if (nestedPlayfields.Count != 0)
enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllEntries));

return enumerable;
}
}

/// <summary>
/// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>.
/// </summary>
Expand Down
7 changes: 5 additions & 2 deletions osu.Game/Screens/Play/GameplayState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ public class GameplayState
public bool HasPassed { get; set; }

/// <summary>
/// Whether the user failed during gameplay. This is only set when the gameplay session has completed due to the fail.
/// Whether the user failed during gameplay and the fail animation has been displayed.
/// </summary>
public bool HasFailed { get; set; }
/// <remarks>
/// In multiplayer, this is never set to <c>true</c> even if the player reached zero health, due to <see cref="PlayerConfiguration.AllowFailAnimation"/> being turned off.
/// </remarks>
public bool ShownFailAnimation { get; set; }
Copy link
Collaborator

@bdach bdach Jan 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree so much with this change and everything it implies.

Any gameplay logic being conditional on "being shown the fail animation" is completely and utterly wrong.

This feels like going in a completely off-base direction. And I think the fix to this entire issue should be local to the multiplayer flow, because it's the one stretching the definition of "fail" to begin with.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approached this initially with the thought that Player has two separate states of failing, each being:

  1. marking score as failed / F rank (determined by HealthProcessor.HasFailed)
  2. presenting fail animation and stopping gameplay (determined by GameplayState.HasFailed, never set to true in multiplayer since Fix multiplayer scores being submitted as pass even if failed #26384).

Therefore I've went with renaming GameplayState.HasFailed to the name in the diff of this conversation, as well as using that state for whether the judgement processors should no longer apply new results.

I will be honest that I didn't pay enough attention at the diff and missed the fact that GameplayState.HasFailed is used in more places than usual, and having the name specifically referring to "presence of fail animation" is indeed sketchy.

I could use better names such as HasStoppedDueToFail, but I've went ahead with your alternative proposal in 64b6110 (making the solution completely local) if discussion for this is not necessary right now (I could imagine another player screen that would require this sort of fix, namely ReplayPlayer when replaying failing multiplayer scores).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could imagine another player screen that would require this sort of fix, namely ReplayPlayer when replaying failing multiplayer scores

I think this is out of scope for now because we currently don't even have a way to tell whether a replay is from multiplayer or from solo. So this has been an issue for a long time now, and it is one caused by the fact that the health processor is allowed to fail a replay, while not considering the fact that in some circumstances the conditions for failure may not be the same.


/// <summary>
/// Whether the user quit gameplay without having either passed or failed.
Expand Down
22 changes: 14 additions & 8 deletions osu.Game/Screens/Play/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,19 @@ private void load(OsuConfigManager config, OsuGameBase game, CancellationToken c

DrawableRuleset.NewResult += r =>
{
if (GameplayState.ShownFailAnimation)
return;

HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r);
GameplayState.ApplyResult(r);
};

DrawableRuleset.RevertResult += r =>
{
if (GameplayState.ShownFailAnimation)
return;

HealthProcessor.RevertResult(r);
ScoreProcessor.RevertResult(r);
};
Expand Down Expand Up @@ -489,7 +495,7 @@ private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)

private void updateGameplayState()
{
bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value && !GameplayState.HasFailed;
bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value && !GameplayState.ShownFailAnimation;
OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered;
localUserPlaying.Value = inGameplay;
}
Expand Down Expand Up @@ -586,7 +592,7 @@ protected bool PerformExit(bool showDialogFirst)
if (showDialogFirst && !pauseOrFailDialogVisible)
{
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
if (ValidForResume && GameplayState.HasFailed)
if (ValidForResume && GameplayState.ShownFailAnimation)
{
failAnimationContainer.FinishTransforms(true);
return false;
Expand Down Expand Up @@ -733,7 +739,7 @@ private void checkScoreCompleted()
}

// Only show the completion screen if the player hasn't failed
if (GameplayState.HasFailed)
if (GameplayState.ShownFailAnimation)
return;

GameplayState.HasPassed = true;
Expand Down Expand Up @@ -922,11 +928,11 @@ private bool onFail()

if (Configuration.AllowFailAnimation)
{
Debug.Assert(!GameplayState.HasFailed);
Debug.Assert(!GameplayState.ShownFailAnimation);
Debug.Assert(!GameplayState.HasPassed);
Debug.Assert(!GameplayState.HasQuit);

GameplayState.HasFailed = true;
GameplayState.ShownFailAnimation = true;

updateGameplayState();

Expand Down Expand Up @@ -1002,13 +1008,13 @@ private void onFailComplete()
// replays cannot be paused and exit immediately
&& !DrawableRuleset.HasReplayLoaded.Value
// cannot pause if we are already in a fail state
&& !GameplayState.HasFailed;
&& !GameplayState.ShownFailAnimation;

private bool canResume =>
// cannot resume from a non-paused state
GameplayClockContainer.IsPaused.Value
// cannot resume if we are already in a fail state
&& !GameplayState.HasFailed
&& !GameplayState.ShownFailAnimation
// already resuming
&& !IsResuming;

Expand Down Expand Up @@ -1142,7 +1148,7 @@ public override bool OnExiting(ScreenExitEvent e)
{
Debug.Assert(resultsDisplayDelegate == null);

if (!GameplayState.HasFailed)
if (!GameplayState.ShownFailAnimation)
GameplayState.HasQuit = true;

if (DrawableRuleset.ReplayScore == null)
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/Screens/Play/PlayerTouchInputDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private void updateState()
if (!touchActive.Value)
return;

if (gameplayState.HasPassed || gameplayState.HasFailed || gameplayState.HasQuit)
if (gameplayState.HasPassed || gameplayState.ShownFailAnimation || gameplayState.HasQuit)
return;

if (gameplayState.Score.ScoreInfo.Mods.OfType<ModTouchDevice>().Any())
Expand Down