Skip to content

Commit

Permalink
Merge pull request #234 from bdach/daily-challenge
Browse files Browse the repository at this point in the history
Announce currently active "daily challenge" playlist to clients
  • Loading branch information
peppy authored May 23, 2024
2 parents 79810ab + e0a8a40 commit 89fd2a1
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 16 deletions.
2 changes: 1 addition & 1 deletion SampleMultiplayerClient/SampleMultiplayerClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="ppy.osu.Game" Version="2024.219.0" />
<PackageReference Include="ppy.osu.Game" Version="2024.523.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion SampleSpectatorClient/SampleSpectatorClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="ppy.osu.Game" Version="2024.219.0" />
<PackageReference Include="ppy.osu.Game" Version="2024.523.0" />
</ItemGroup>

</Project>
87 changes: 87 additions & 0 deletions osu.Server.Spectator.Tests/DailyChallengeUpdaterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;
using osu.Game.Online.Metadata;
using osu.Server.Spectator.Database;
using osu.Server.Spectator.Database.Models;
using osu.Server.Spectator.Hubs.Metadata;
using Xunit;

namespace osu.Server.Spectator.Tests
{
public class DailyChallengeUpdaterTest
{
private readonly Mock<ILoggerFactory> loggerFactoryMock;
private readonly Mock<IDatabaseFactory> databaseFactoryMock;
private readonly Mock<IDatabaseAccess> databaseAccessMock;
private readonly Mock<IHubContext<MetadataHub>> metadataHubContextMock;
private readonly Mock<IClientProxy> allClientsProxy;

public DailyChallengeUpdaterTest()
{
loggerFactoryMock = new Mock<ILoggerFactory>();
loggerFactoryMock.Setup(factory => factory.CreateLogger(It.IsAny<string>()))
.Returns(new Mock<ILogger>().Object);

databaseFactoryMock = new Mock<IDatabaseFactory>();
databaseAccessMock = new Mock<IDatabaseAccess>();
databaseFactoryMock.Setup(factory => factory.GetInstance()).Returns(databaseAccessMock.Object);

metadataHubContextMock = new Mock<IHubContext<MetadataHub>>();
allClientsProxy = new Mock<IClientProxy>();
metadataHubContextMock.Setup(ctx => ctx.Clients.All).Returns(allClientsProxy.Object);
}

[Fact]
public async Task TestChangeTracking()
{
databaseAccessMock.Setup(db => db.GetActiveDailyChallengeRoomsAsync())
.ReturnsAsync([new multiplayer_room { id = 4, category = room_category.daily_challenge }]);

var updater = new DailyChallengeUpdater(
loggerFactoryMock.Object,
databaseFactoryMock.Object,
metadataHubContextMock.Object)
{
UpdateInterval = 50
};

var task = updater.StartAsync(default);
await Task.Delay(100);

allClientsProxy.Verify(proxy => proxy.SendCoreAsync(
nameof(IMetadataClient.DailyChallengeUpdated),
It.Is<object[]>(args => ((DailyChallengeInfo?)args![0]).Value.RoomID == 4),
It.IsAny<CancellationToken>()),
Times.Once);

databaseAccessMock.Setup(db => db.GetActiveDailyChallengeRoomsAsync())
.ReturnsAsync([]);
await Task.Delay(100);

allClientsProxy.Verify(proxy => proxy.SendCoreAsync(
nameof(IMetadataClient.DailyChallengeUpdated),
It.Is<object?[]>(args => args[0] == null),
It.IsAny<CancellationToken>()),
Times.Once);

databaseAccessMock.Setup(db => db.GetActiveDailyChallengeRoomsAsync())
.ReturnsAsync([new multiplayer_room { id = 5, category = room_category.daily_challenge }]);
await Task.Delay(100);

allClientsProxy.Verify(proxy => proxy.SendCoreAsync(
nameof(IMetadataClient.DailyChallengeUpdated),
It.Is<object[]>(args => ((DailyChallengeInfo?)args![0]).HasValue && ((DailyChallengeInfo?)args[0]).Value.RoomID == 5),
It.IsAny<CancellationToken>()),
Times.Once);

await updater.StopAsync(default);
await task;
}
}
}
2 changes: 1 addition & 1 deletion osu.Server.Spectator.Tests/MetadataHubTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public MetadataHubTest()
var databaseFactory = new Mock<IDatabaseFactory>();
databaseFactory.Setup(factory => factory.GetInstance()).Returns(mockDatabase.Object);

hub = new MetadataHub(cache, userStates, databaseFactory.Object);
hub = new MetadataHub(cache, userStates, databaseFactory.Object, new Mock<IDailyChallengeUpdater>().Object);

var mockContext = new Mock<HubCallerContext>();
mockContext.Setup(ctx => ctx.UserIdentifier).Returns(user_id.ToString());
Expand Down
12 changes: 12 additions & 0 deletions osu.Server.Spectator/Database/DatabaseAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,18 @@ public async Task<IEnumerable<chat_filter>> GetAllChatFiltersAsync()
return await connection.QueryAsync<chat_filter>("SELECT * FROM `chat_filters`");
}

public async Task<IEnumerable<multiplayer_room>> GetActiveDailyChallengeRoomsAsync()
{
var connection = await getConnectionAsync();

return await connection.QueryAsync<multiplayer_room>(
"SELECT * FROM `multiplayer_rooms` "
+ "WHERE `category` = 'daily_challenge' "
+ "AND `type` = 'playlists' "
+ "AND `starts_at` <= NOW() "
+ "AND `ends_at` > NOW()");
}

public void Dispose()
{
openConnection?.Dispose();
Expand Down
5 changes: 5 additions & 0 deletions osu.Server.Spectator/Database/IDatabaseAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,10 @@ public interface IDatabaseAccess : IDisposable
/// Retrieves all <see cref="chat_filter"/>s from the database.
/// </summary>
Task<IEnumerable<chat_filter>> GetAllChatFiltersAsync();

/// <summary>
/// Retrieves all active rooms from the <see cref="room_category.daily_challenge"/> category.
/// </summary>
Task<IEnumerable<multiplayer_room>> GetActiveDailyChallengeRoomsAsync();
}
}
3 changes: 1 addition & 2 deletions osu.Server.Spectator/Database/Models/multiplayer_room.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using osu.Game.Online.Rooms;

// ReSharper disable InconsistentNaming (matches database table)

Expand All @@ -23,7 +22,7 @@ public class multiplayer_room
public DateTimeOffset? created_at { get; set; }
public DateTimeOffset? updated_at { get; set; }
public DateTimeOffset? deleted_at { get; set; }
public RoomCategory category { get; set; }
public room_category category { get; set; }
public database_match_type type { get; set; }
public database_queue_mode queue_mode { get; set; }
public ushort auto_start_duration { get; set; }
Expand Down
17 changes: 17 additions & 0 deletions osu.Server.Spectator/Database/Models/room_category.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;

namespace osu.Server.Spectator.Database.Models
{
// ReSharper disable once InconsistentNaming
[Serializable]
public enum room_category
{
normal,
spotlights,
featured_artist,
daily_challenge,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public static IServiceCollection AddHubEntities(this IServiceCollection serviceC
.AddSingleton<ScoreUploader>()
.AddSingleton<IScoreProcessedSubscriber, ScoreProcessedSubscriber>()
.AddSingleton<BuildUserCountUpdater>()
.AddSingleton<ChatFilters>();
.AddSingleton<ChatFilters>()
.AddSingleton<IDailyChallengeUpdater, DailyChallengeUpdater>()
.AddHostedService<IDailyChallengeUpdater>(ctx => ctx.GetRequiredService<IDailyChallengeUpdater>());
}

/// <summary>
Expand Down
88 changes: 88 additions & 0 deletions osu.Server.Spectator/Hubs/Metadata/DailyChallengeUpdater.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using osu.Game.Online.Metadata;
using osu.Server.Spectator.Database;

namespace osu.Server.Spectator.Hubs.Metadata
{
public interface IDailyChallengeUpdater : IHostedService
{
DailyChallengeInfo? Current { get; }
}

public class DailyChallengeUpdater : BackgroundService, IDailyChallengeUpdater
{
/// <summary>
/// Amount of time (in milliseconds) between subsequent polls for the current beatmap of the day.
/// </summary>
public int UpdateInterval = 60_000;

public DailyChallengeInfo? Current { get; private set; }

private readonly ILogger logger;
private readonly IDatabaseFactory databaseFactory;
private readonly IHubContext<MetadataHub> hubContext;

public DailyChallengeUpdater(
ILoggerFactory loggerFactory,
IDatabaseFactory databaseFactory,
IHubContext<MetadataHub> hubContext)
{
logger = loggerFactory.CreateLogger(nameof(DailyChallengeUpdater));
this.databaseFactory = databaseFactory;
this.hubContext = hubContext;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await updateDailyChallengeInfo(stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to update beatmap of the day");
}

await Task.Delay(UpdateInterval, stoppingToken);
}
}

private async Task updateDailyChallengeInfo(CancellationToken cancellationToken)
{
using var db = databaseFactory.GetInstance();

var activeRooms = (await db.GetActiveDailyChallengeRoomsAsync()).ToList();

if (activeRooms.Count > 1)
{
logger.LogWarning("More than one active 'beatmap of the day' room detected (ids: {roomIds}). Will only use the first one.",
string.Join(',', activeRooms.Select(room => room.id)));
}

DailyChallengeInfo? newInfo = null;

var activeRoom = activeRooms.FirstOrDefault();

if (activeRoom?.id != null)
newInfo = new DailyChallengeInfo { RoomID = activeRoom.id };

if (!Current.Equals(newInfo))
{
logger.LogInformation("Broadcasting 'beatmap of the day' room change from id {oldRoomID} to {newRoomId}", Current?.RoomID, newInfo?.RoomID);
Current = newInfo;
await hubContext.Clients.All.SendAsync(nameof(IMetadataClient.DailyChallengeUpdated), Current, cancellationToken);
}
}
}
}
6 changes: 5 additions & 1 deletion osu.Server.Spectator/Hubs/Metadata/MetadataHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ namespace osu.Server.Spectator.Hubs.Metadata
public class MetadataHub : StatefulUserHub<IMetadataClient, MetadataClientState>, IMetadataServer
{
private readonly IDatabaseFactory databaseFactory;
private readonly IDailyChallengeUpdater dailyChallengeUpdater;

internal const string ONLINE_PRESENCE_WATCHERS_GROUP = "metadata:online-presence-watchers";

public MetadataHub(
IDistributedCache cache,
EntityStore<MetadataClientState> userStates,
IDatabaseFactory databaseFactory)
IDatabaseFactory databaseFactory,
IDailyChallengeUpdater dailyChallengeUpdater)
: base(cache, userStates)
{
this.databaseFactory = databaseFactory;
this.dailyChallengeUpdater = dailyChallengeUpdater;
}

public override async Task OnConnectedAsync()
Expand All @@ -49,6 +52,7 @@ public override async Task OnConnectedAsync()

usage.Item = new MetadataClientState(Context.ConnectionId, Context.GetUserId(), versionHash);
await broadcastUserPresenceUpdate(usage.Item.UserId, usage.Item.ToUserPresence());
await Clients.Caller.DailyChallengeUpdated(dailyChallengeUpdater.Current);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,19 @@ public ScoreProcessedSubscriber(
timer.Start();

subscriber = redis.GetSubscriber();
subscriber.Subscribe("osu-channel:score:processed", (_, message) => onMessageReceived(message));
subscriber.Subscribe(new RedisChannel("osu-channel:score:processed", RedisChannel.PatternMode.Literal), (_, message) => onMessageReceived(message));

logger = Logger.GetLogger(nameof(ScoreProcessedSubscriber));
}

private void onMessageReceived(string message)
private void onMessageReceived(string? message)
{
try
{
var scoreProcessed = JsonConvert.DeserializeObject<ScoreProcessed>(message);
if (string.IsNullOrEmpty(message))
return;

ScoreProcessed? scoreProcessed = JsonConvert.DeserializeObject<ScoreProcessed>(message);

if (scoreProcessed == null)
return;
Expand Down
12 changes: 6 additions & 6 deletions osu.Server.Spectator/osu.Server.Spectator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="ppy.osu.Game" Version="2024.219.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2024.219.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2024.219.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.219.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2024.219.0" />
<PackageReference Include="ppy.osu.Server.OsuQueueProcessor" Version="2023.1207.0" />
<PackageReference Include="ppy.osu.Game" Version="2024.523.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2024.523.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2024.523.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.523.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2024.523.0" />
<PackageReference Include="ppy.osu.Server.OsuQueueProcessor" Version="2024.507.0" />
<PackageReference Include="Sentry.AspNetCore" Version="4.3.0" />
</ItemGroup>

Expand Down

0 comments on commit 89fd2a1

Please sign in to comment.