Skip to content
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

Add support for creating, joining, and parting osu!web rooms via interop #265

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ To deploy this as part of a full osu! server stack deployment, [this wiki page](

For advanced testing purposes.

| Envvar name | Description | Default value |
| :- | :- | :- |
| `SAVE_REPLAYS` | Whether to save received replay frames from clients to replay files. `1` to enable, any other value to disable. | `""` (disabled) |
| `REPLAY_UPLOAD_THREADS` | Number of threads to use when uploading complete replays. Must be positive number. | `1` |
| `REPLAYS_PATH` | Local path to store complete replay files (`.osr`) to. Only used if [`FileScoreStorage`](https://github.com/ppy/osu-server-spectator/blob/master/osu.Server.Spectator/Storage/FileScoreStorage.cs) is active. | `./replays/` |
| `S3_KEY` | An access key ID to use for uploading replays to [AWS S3](https://aws.amazon.com/s3/). Only used if [`S3ScoreStorage`](https://github.com/ppy/osu-server-spectator/blob/master/osu.Server.Spectator/Storage/S3ScoreStorage.cs) is active. | `""` |
| `S3_SECRET` | The secret access key to use for uploading replays to [AWS S3](https://aws.amazon.com/s3/). Only used if [`S3ScoreStorage`](https://github.com/ppy/osu-server-spectator/blob/master/osu.Server.Spectator/Storage/S3ScoreStorage.cs) is active. | `""` |
| `REPLAYS_BUCKET` | The name of the [AWS S3](https://aws.amazon.com/s3/) bucket to upload replays to. Only used if [`S3ScoreStorage`](https://github.com/ppy/osu-server-spectator/blob/master/osu.Server.Spectator/Storage/S3ScoreStorage.cs) is active. | `""` |
| `TRACK_BUILD_USER_COUNTS` | Whether to track how many users are on a particular build of the game and report that information to the database at `DB_{HOST,PORT}`. `1` to enable, any other value to disable. | `""` (disabled) |
| `SERVER_PORT` | Which port the server should listen on for incoming connections. | `80` |
| `REDIS_HOST` | Connection string to `osu-web` Redis instance. | `localhost` |
| `DD_AGENT_HOST` | Hostname under which the [Datadog](https://www.datadoghq.com/) agent host can be found. | `localhost` |
| `DB_HOST` | Hostname under which the `osu-web` MySQL instance can be found. | `localhost` |
| `DB_PORT` | Port under which the `osu-web` MySQL instance can be found. | `3306` |
| `DB_USER` | Username to use when logging into the `osu-web` MySQL instance. | `osuweb` |
| Envvar name | Description | Default value |
| :- | :- |:------------------|
| `SAVE_REPLAYS` | Whether to save received replay frames from clients to replay files. `1` to enable, any other value to disable. | `""` (disabled) |
| `REPLAY_UPLOAD_THREADS` | Number of threads to use when uploading complete replays. Must be positive number. | `1` |
| `REPLAYS_PATH` | Local path to store complete replay files (`.osr`) to. Only used if [`FileScoreStorage`](https://github.com/ppy/osu-server-spectator/blob/master/osu.Server.Spectator/Storage/FileScoreStorage.cs) is active. | `./replays/` |
| `S3_KEY` | An access key ID to use for uploading replays to [AWS S3](https://aws.amazon.com/s3/). Only used if [`S3ScoreStorage`](https://github.com/ppy/osu-server-spectator/blob/master/osu.Server.Spectator/Storage/S3ScoreStorage.cs) is active. | `""` |
| `S3_SECRET` | The secret access key to use for uploading replays to [AWS S3](https://aws.amazon.com/s3/). Only used if [`S3ScoreStorage`](https://github.com/ppy/osu-server-spectator/blob/master/osu.Server.Spectator/Storage/S3ScoreStorage.cs) is active. | `""` |
| `REPLAYS_BUCKET` | The name of the [AWS S3](https://aws.amazon.com/s3/) bucket to upload replays to. Only used if [`S3ScoreStorage`](https://github.com/ppy/osu-server-spectator/blob/master/osu.Server.Spectator/Storage/S3ScoreStorage.cs) is active. | `""` |
| `TRACK_BUILD_USER_COUNTS` | Whether to track how many users are on a particular build of the game and report that information to the database at `DB_{HOST,PORT}`. `1` to enable, any other value to disable. | `""` (disabled) |
| `SERVER_PORT` | Which port the server should listen on for incoming connections. | `80` |
| `REDIS_HOST` | Connection string to `osu-web` Redis instance. | `localhost` |
| `DD_AGENT_HOST` | Hostname under which the [Datadog](https://www.datadoghq.com/) agent host can be found. | `localhost` |
| `DB_HOST` | Hostname under which the `osu-web` MySQL instance can be found. | `localhost` |
| `DB_PORT` | Port under which the `osu-web` MySQL instance can be found. | `3306` |
| `DB_USER` | Username to use when logging into the `osu-web` MySQL instance. | `osuweb` |
| `LEGACY_IO_DOMAIN` | The root URL of the osu-web instance to which legacy IO call should be submitted | `null` (required) |
| `SHARED_INTEROP_SECRET` | The value of the same environment variable that the target osu-web instance specifies in `.env`. | `null` (required) |
11 changes: 5 additions & 6 deletions SampleMultiplayerClient/MultiplayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,14 @@ public MultiplayerClient(HubConnection connection, int userId)

public MultiplayerRoom? Room { get; private set; }

public Task<MultiplayerRoom> CreateRoom(MultiplayerRoom room)
=> connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.CreateRoom), room);

public async Task<MultiplayerRoom> JoinRoom(long roomId)
{
return await JoinRoomWithPassword(roomId, string.Empty);
}
=> await JoinRoomWithPassword(roomId, string.Empty);

public async Task<MultiplayerRoom> JoinRoomWithPassword(long roomId, string? password = null)
{
return Room = await connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
}
=> Room = await connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);

public async Task LeaveRoom()
{
Expand Down
8 changes: 7 additions & 1 deletion osu.Server.Spectator.Tests/Multiplayer/MultiplayerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using osu.Server.Spectator.Database.Models;
using osu.Server.Spectator.Entities;
using osu.Server.Spectator.Hubs.Multiplayer;
using osu.Server.Spectator.Services;

namespace osu.Server.Spectator.Tests.Multiplayer
{
Expand All @@ -37,6 +38,8 @@ public abstract class MultiplayerTest
protected readonly Mock<IDatabaseFactory> DatabaseFactory;
protected readonly Mock<IDatabaseAccess> Database;

protected readonly Mock<ILegacyIO> LegacyIO;

/// <summary>
/// A general non-gameplay receiver for the room with ID <see cref="ROOM_ID"/>.
/// </summary>
Expand Down Expand Up @@ -130,13 +133,16 @@ protected MultiplayerTest()
loggerFactoryMock.Setup(factory => factory.CreateLogger(It.IsAny<string>()))
.Returns(new Mock<ILogger>().Object);

LegacyIO = new Mock<ILegacyIO>();

Hub = new TestMultiplayerHub(
loggerFactoryMock.Object,
Rooms,
UserStates,
DatabaseFactory.Object,
new ChatFilters(DatabaseFactory.Object),
hubContext.Object);
hubContext.Object,
LegacyIO.Object);
Hub.Groups = Groups.Object;
Hub.Clients = Clients.Object;

Expand Down
44 changes: 44 additions & 0 deletions osu.Server.Spectator.Tests/Multiplayer/RoomInteropTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Moq;
using osu.Game.Online.Multiplayer;
using Xunit;

namespace osu.Server.Spectator.Tests.Multiplayer
{
public class RoomInteropTest : MultiplayerTest
{
[Fact]
public async Task CreateRoom()
{
LegacyIO.Setup(io => io.CreateRoomAsync(It.IsAny<int>(), It.IsAny<MultiplayerRoom>()))
.ReturnsAsync(() => ROOM_ID);

await Hub.CreateRoom(new MultiplayerRoom(0));
LegacyIO.Verify(io => io.CreateRoomAsync(USER_ID, It.IsAny<MultiplayerRoom>()), Times.Once);
LegacyIO.Verify(io => io.JoinRoomAsync(ROOM_ID, USER_ID), Times.Once);

using (var usage = await Hub.GetRoom(ROOM_ID))
{
Assert.NotNull(usage.Item);
Assert.Equal(USER_ID, usage.Item.Users.Single().UserID);
}
}

[Fact]
public async Task LeaveRoom()
{
await Hub.JoinRoom(ROOM_ID);
LegacyIO.Verify(io => io.PartRoomAsync(ROOM_ID, USER_ID), Times.Never);

await Hub.LeaveRoom();
LegacyIO.Verify(io => io.PartRoomAsync(ROOM_ID, USER_ID), Times.Once);

await Assert.ThrowsAsync<KeyNotFoundException>(() => Hub.GetRoom(ROOM_ID));
}
}
}
6 changes: 4 additions & 2 deletions osu.Server.Spectator.Tests/Multiplayer/TestMultiplayerHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using osu.Server.Spectator.Database;
using osu.Server.Spectator.Entities;
using osu.Server.Spectator.Hubs.Multiplayer;
using osu.Server.Spectator.Services;

namespace osu.Server.Spectator.Tests.Multiplayer
{
Expand All @@ -19,8 +20,9 @@ public TestMultiplayerHub(
EntityStore<MultiplayerClientState> users,
IDatabaseFactory databaseFactory,
ChatFilters chatFilters,
IHubContext<MultiplayerHub> hubContext)
: base(loggerFactory, rooms, users, databaseFactory, chatFilters, hubContext)
IHubContext<MultiplayerHub> hubContext,
ILegacyIO legacyIO)
: base(loggerFactory, rooms, users, databaseFactory, chatFilters, hubContext, legacyIO)
{
}

Expand Down
1 change: 1 addition & 0 deletions osu.Server.Spectator.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HUD/@EntryIndexedValue">HUD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ID/@EntryIndexedValue">ID</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IL/@EntryIndexedValue">IL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IO/@EntryIndexedValue">IO</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IOS/@EntryIndexedValue">IOS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IPC/@EntryIndexedValue">IPC</s:String>
Expand Down
6 changes: 6 additions & 0 deletions osu.Server.Spectator/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public static class AppSettings
public static string DatabaseUser { get; }
public static string DatabasePort { get; }

public static string LegacyIODomain { get; }
public static string SharedInteropSecret { get; }

static AppSettings()
{
SaveReplays = Environment.GetEnvironmentVariable("SAVE_REPLAYS") == "1";
Expand All @@ -53,6 +56,9 @@ static AppSettings()
DatabaseHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost";
DatabaseUser = Environment.GetEnvironmentVariable("DB_USER") ?? "osuweb";
DatabasePort = Environment.GetEnvironmentVariable("DB_PORT") ?? "3306";

LegacyIODomain = Environment.GetEnvironmentVariable("LEGACY_IO_DOMAIN") ?? string.Empty;
SharedInteropSecret = Environment.GetEnvironmentVariable("SHARED_INTEROP_SECRET") ?? string.Empty;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using osu.Server.Spectator.Hubs.Metadata;
using osu.Server.Spectator.Hubs.Multiplayer;
using osu.Server.Spectator.Hubs.Spectator;
using osu.Server.Spectator.Services;
using osu.Server.Spectator.Storage;
using StackExchange.Redis;

Expand All @@ -17,7 +18,9 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddHubEntities(this IServiceCollection serviceCollection)
{
return serviceCollection.AddSingleton<EntityStore<SpectatorClientState>>()
return serviceCollection.AddHttpClient()
.AddTransient<ILegacyIO, LegacyIO>()
.AddSingleton<EntityStore<SpectatorClientState>>()
.AddSingleton<EntityStore<MultiplayerClientState>>()
.AddSingleton<EntityStore<ServerMultiplayerRoom>>()
.AddSingleton<EntityStore<ConnectionState>>()
Expand Down
25 changes: 19 additions & 6 deletions osu.Server.Spectator/Hubs/Multiplayer/MultiplayerHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using osu.Server.Spectator.Database.Models;
using osu.Server.Spectator.Entities;
using osu.Server.Spectator.Extensions;
using osu.Server.Spectator.Services;

namespace osu.Server.Spectator.Hubs.Multiplayer
{
Expand All @@ -25,34 +26,44 @@ public class MultiplayerHub : StatefulUserHub<IMultiplayerClient, MultiplayerCli
protected readonly MultiplayerHubContext HubContext;
private readonly IDatabaseFactory databaseFactory;
private readonly ChatFilters chatFilters;
private readonly ILegacyIO legacyIO;

public MultiplayerHub(
ILoggerFactory loggerFactory,
EntityStore<ServerMultiplayerRoom> rooms,
EntityStore<MultiplayerClientState> users,
IDatabaseFactory databaseFactory,
ChatFilters chatFilters,
IHubContext<MultiplayerHub> hubContext)
IHubContext<MultiplayerHub> hubContext,
ILegacyIO legacyIO)
: base(loggerFactory, users)
{
Rooms = rooms;
this.databaseFactory = databaseFactory;
this.chatFilters = chatFilters;
this.legacyIO = legacyIO;
HubContext = new MultiplayerHubContext(hubContext, rooms, users, databaseFactory, loggerFactory);
}

public async Task<MultiplayerRoom> CreateRoom(MultiplayerRoom room)
{
Log($"{Context.GetUserId()} creating room");
return await JoinRoomWithPassword(await legacyIO.CreateRoomAsync(Context.GetUserId(), room), room.Settings.Password);
}

public Task<MultiplayerRoom> JoinRoom(long roomId) => JoinRoomWithPassword(roomId, string.Empty);

public async Task<MultiplayerRoom> JoinRoomWithPassword(long roomId, string password)
{
Log($"Attempting to join room {roomId}");

bool isRestricted;
using (var db = databaseFactory.GetInstance())
isRestricted = await db.IsUserRestrictedAsync(Context.GetUserId());
{
if (await db.IsUserRestrictedAsync(Context.GetUserId()))
throw new InvalidStateException("Can't join a room when restricted.");
}

if (isRestricted)
throw new InvalidStateException("Can't join a room when restricted.");
await legacyIO.JoinRoomAsync(roomId, Context.GetUserId());

using (var userUsage = await GetOrCreateLocalUserState())
{
Expand Down Expand Up @@ -720,8 +731,8 @@ private async Task removeDatabaseUser(MultiplayerRoom room, MultiplayerRoomUser

protected override async Task CleanUpState(MultiplayerClientState state)
{
await leaveRoom(state, true);
await base.CleanUpState(state);
await leaveRoom(state, true);
Comment on lines -723 to +735
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Meaningless change because the base implementation is empty. I just thought I'd make this consistent with other implementations, seeing as leaveRoom can throw an exception.

}

private async Task setNewHost(MultiplayerRoom room, MultiplayerRoomUser newHost)
Expand Down Expand Up @@ -883,6 +894,8 @@ private async Task<ItemUsage<ServerMultiplayerRoom>> getLocalUserRoom(Multiplaye

private async Task leaveRoom(MultiplayerClientState state, bool wasKick)
{
await legacyIO.PartRoomAsync(state.CurrentRoomID, state.UserId);

using (var roomUsage = await getLocalUserRoom(state))
await leaveRoom(state, roomUsage, wasKick);
}
Expand Down
33 changes: 33 additions & 0 deletions osu.Server.Spectator/Services/ILegacyIO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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.Tasks;
using osu.Game.Online.Multiplayer;

namespace osu.Server.Spectator.Services
{
public interface ILegacyIO
{
/// <summary>
/// Creates an osu!web Room.
/// </summary>
/// <param name="userId">The ID of the user that wants to create the room.</param>
/// <param name="room">The room.</param>
/// <returns>The room's ID.</returns>
Task<long> CreateRoomAsync(int userId, MultiplayerRoom room);

/// <summary>
/// Joins an osu!web Room.
/// </summary>
/// <param name="roomId">The ID of the room to join.</param>
/// <param name="userId">The ID of the user wanting to join the room.</param>
Task JoinRoomAsync(long roomId, int userId);

/// <summary>
/// Parts an osu!web Room.
/// </summary>
/// <param name="roomId">The ID of the room to part.</param>
/// <param name="userId">The ID of the user wanting to part the room.</param>
Task PartRoomAsync(long roomId, int userId);
}
}
Loading