Skip to content

Commit

Permalink
Merge pull request #245 from bdach/handle-mid-play-mod-changes
Browse files Browse the repository at this point in the history
Handle mid play mod changes
  • Loading branch information
peppy authored Oct 23, 2024
2 parents 9f8a00c + def060e commit 2beec71
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 0 deletions.
108 changes: 108 additions & 0 deletions osu.Server.Spectator.Tests/SpectatorHubTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Server.Spectator.Database;
Expand Down Expand Up @@ -208,6 +211,111 @@ await hub.EndPlaySession(new SpectatorState
mockReceiver.Verify(clients => clients.UserFinishedPlaying(streamer_id, It.Is<SpectatorState>(m => m.State == SpectatedUserState.Quit)), Times.Once());
}

[Fact]
public async Task ModChangesDuringPlayAreHandled()
{
scoreUploader.SaveReplays = true;

Mock<IHubCallerClients<ISpectatorClient>> mockClients = new Mock<IHubCallerClients<ISpectatorClient>>();
Mock<ISpectatorClient> mockReceiver = new Mock<ISpectatorClient>();
mockClients.Setup(clients => clients.All).Returns(mockReceiver.Object);
mockClients.Setup(clients => clients.Group(SpectatorHub.GetGroupId(streamer_id))).Returns(mockReceiver.Object);

Mock<HubCallerContext> mockContext = new Mock<HubCallerContext>();

mockContext.Setup(context => context.UserIdentifier).Returns(streamer_id.ToString());
hub.Context = mockContext.Object;
hub.Clients = mockClients.Object;

mockDatabase.Setup(db => db.GetScoreFromTokenAsync(1234)).Returns(Task.FromResult<SoloScore?>(new SoloScore
{
id = 456,
passed = true
}));

await hub.BeginPlaySession(1234, new SpectatorState
{
BeatmapID = beatmap_id,
RulesetID = 0,
State = SpectatedUserState.Playing,
});

await hub.SendFrameData(new FrameDataBundle(
new FrameHeader(new ScoreInfo
{
Mods = [new OsuModTouchDevice()],
Statistics = new Dictionary<HitResult, int> { [HitResult.Great] = 1 }
}, new ScoreProcessorStatistics()),
new[] { new LegacyReplayFrame(1234, 0, 0, ReplayButtonState.None) }));

await hub.EndPlaySession(new SpectatorState
{
BeatmapID = beatmap_id,
RulesetID = 0,
State = SpectatedUserState.Quit,
});

await uploadsCompleteAsync();

mockScoreStorage.Verify(s => s.WriteAsync(It.Is<Score>(s => s.ScoreInfo.Mods.Any(m => m is OsuModTouchDevice))), Times.Once);
mockReceiver.Verify(clients => clients.UserFinishedPlaying(streamer_id, It.Is<SpectatorState>(m => m.State == SpectatedUserState.Quit)), Times.Once());
}

[Fact]
public async Task FrameBundlesFromOldClientsWithoutModsHandledCorrectly()
{
scoreUploader.SaveReplays = true;

Mock<IHubCallerClients<ISpectatorClient>> mockClients = new Mock<IHubCallerClients<ISpectatorClient>>();
Mock<ISpectatorClient> mockReceiver = new Mock<ISpectatorClient>();
mockClients.Setup(clients => clients.All).Returns(mockReceiver.Object);
mockClients.Setup(clients => clients.Group(SpectatorHub.GetGroupId(streamer_id))).Returns(mockReceiver.Object);

Mock<HubCallerContext> mockContext = new Mock<HubCallerContext>();

mockContext.Setup(context => context.UserIdentifier).Returns(streamer_id.ToString());
hub.Context = mockContext.Object;
hub.Clients = mockClients.Object;

mockDatabase.Setup(db => db.GetScoreFromTokenAsync(1234)).Returns(Task.FromResult<SoloScore?>(new SoloScore
{
id = 456,
passed = true
}));

await hub.BeginPlaySession(1234, new SpectatorState
{
BeatmapID = beatmap_id,
RulesetID = 0,
State = SpectatedUserState.Playing,
Mods = [new APIMod(new OsuModDoubleTime())]
});

var frameHeader = new FrameHeader(new ScoreInfo
{
Statistics = new Dictionary<HitResult, int> { [HitResult.Great] = 1 }
}, new ScoreProcessorStatistics())
{
Mods = null, // simulate older client that did not send this property over wire
};

await hub.SendFrameData(new FrameDataBundle(
frameHeader,
new[] { new LegacyReplayFrame(1234, 0, 0, ReplayButtonState.None) }));

await hub.EndPlaySession(new SpectatorState
{
BeatmapID = beatmap_id,
RulesetID = 0,
State = SpectatedUserState.Quit,
});

await uploadsCompleteAsync();

mockScoreStorage.Verify(s => s.WriteAsync(It.Is<Score>(s => s.ScoreInfo.Mods.Any(m => m is OsuModDoubleTime))), Times.Once);
mockReceiver.Verify(clients => clients.UserFinishedPlaying(streamer_id, It.Is<SpectatorState>(m => m.State == SpectatedUserState.Quit)), Times.Once());
}

[Theory]
[InlineData(false)]
[InlineData(true)]
Expand Down
5 changes: 5 additions & 0 deletions osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ public async Task SendFrameData(FrameDataBundle data)
score.ScoreInfo.Combo = data.Header.Combo;
score.ScoreInfo.TotalScore = data.Header.TotalScore;

// null here means the frame bundle is from an old client that can't send mod data
// can be removed (along with making property non-nullable on `FrameDataBundle`) 20250407
if (data.Header.Mods != null)
score.ScoreInfo.APIMods = data.Header.Mods;

score.Replay.Frames.AddRange(data.Frames);

await Clients.Group(GetGroupId(Context.GetUserId())).UserSentFrames(Context.GetUserId(), data);
Expand Down

0 comments on commit 2beec71

Please sign in to comment.