Skip to content

Commit 00e1879

Browse files
author
Copilot
authored
Merge ppy/osu master (7 commits): ranked play/matchmaking + WebSocket server skeleton
Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
2 parents 21ce86a + 7b0e5ec commit 00e1879

23 files changed

Lines changed: 1049 additions & 115 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using Newtonsoft.Json;
5+
6+
namespace osu.Desktop.IPC.Messages
7+
{
8+
public class HitCountMessage : OsuWebSocketMessage
9+
{
10+
[JsonProperty("new_hits")]
11+
public long NewHits { get; init; }
12+
}
13+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using Newtonsoft.Json;
5+
using osu.Framework.Extensions.TypeExtensions;
6+
7+
namespace osu.Desktop.IPC.Messages
8+
{
9+
public abstract class OsuWebSocketMessage
10+
{
11+
[JsonProperty("type")]
12+
public string Type { get; }
13+
14+
protected OsuWebSocketMessage()
15+
{
16+
Type = GetType().ReadableName();
17+
}
18+
}
19+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Threading;
7+
using osu.Desktop.IPC.Messages;
8+
using osu.Framework.Allocation;
9+
using osu.Framework.Bindables;
10+
using osu.Framework.Extensions;
11+
using osu.Framework.Graphics;
12+
using osu.Framework.Logging;
13+
using osu.Game.Configuration;
14+
using osu.Game.IPC;
15+
using osu.Game.Online.Multiplayer;
16+
using osu.Game.Rulesets.Scoring;
17+
using osu.Game.Scoring;
18+
using JsonConvert = Newtonsoft.Json.JsonConvert;
19+
20+
namespace osu.Desktop.IPC
21+
{
22+
public partial class OsuWebSocketProvider : Component
23+
{
24+
private WebSocketServer? server;
25+
private readonly Bindable<ScoreInfo> lastLocalScore = new Bindable<ScoreInfo>();
26+
27+
[BackgroundDependencyLoader]
28+
private void load(SessionStatics sessionStatics)
29+
{
30+
server = new WebSocketServer(49727);
31+
server.StartAsync().FireAndForget(onError: ex => Logger.Error(ex, "Failed to start websocket"));
32+
33+
sessionStatics.BindWith(Static.LastLocalUserScore, lastLocalScore);
34+
}
35+
36+
protected override void LoadComplete()
37+
{
38+
base.LoadComplete();
39+
40+
lastLocalScore.BindValueChanged(val =>
41+
{
42+
if (val.NewValue == null)
43+
return;
44+
45+
if (server?.IsRunning != true)
46+
return;
47+
48+
var msg = new HitCountMessage { NewHits = val.NewValue.Statistics.Where(kv => kv.Key.IsBasic() && kv.Key.IsHit()).Sum(kv => kv.Value) };
49+
broadcast(msg);
50+
});
51+
}
52+
53+
private void broadcast(OsuWebSocketMessage message)
54+
{
55+
if (server?.IsRunning != true)
56+
return;
57+
58+
string messageString = JsonConvert.SerializeObject(message);
59+
server.BroadcastAsync(messageString).FireAndForget();
60+
}
61+
62+
protected override void Dispose(bool isDisposing)
63+
{
64+
base.Dispose(isDisposing);
65+
66+
if (server?.IsRunning == true)
67+
{
68+
var cts = new CancellationTokenSource();
69+
cts.CancelAfter(TimeSpan.FromSeconds(10));
70+
server.StopAsync(cts.Token).WaitSafely();
71+
server = null;
72+
}
73+
}
74+
}
75+
}

osu.Desktop/OsuGameDesktop.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ internal partial class OsuGameDesktop : OsuGame
3535

3636
public bool IsFirstRun { get; init; }
3737

38+
public bool EnableWebSocketServer { get; init; }
39+
3840
public OsuGameDesktop(string[]? args = null)
3941
: base(args)
4042
{
@@ -148,6 +150,9 @@ protected override void LoadComplete()
148150

149151
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
150152
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
153+
154+
if (EnableWebSocketServer)
155+
Add(new OsuWebSocketProvider());
151156
}
152157

153158
public override void SetHost(GameHost host)

osu.Desktop/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ public static void Main(string[] args)
140140
{
141141
host.Run(new OsuGameDesktop(args)
142142
{
143-
IsFirstRun = isFirstRun
143+
IsFirstRun = isFirstRun,
144+
EnableWebSocketServer = Environment.GetEnvironmentVariable("OSU_WEBSOCKET_SERVER") == "1",
144145
});
145146
}
146147
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Net.WebSockets;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using osu.Game.IPC;
9+
10+
namespace osu.Game.Tests.IPC
11+
{
12+
public sealed class WebSocketClient : IDisposable
13+
{
14+
public event Action<string>? MessageReceived;
15+
public event Action? Closed;
16+
17+
private readonly int port;
18+
private WebSocketChannel? channel;
19+
20+
public WebSocketClient(int port)
21+
{
22+
this.port = port;
23+
}
24+
25+
public async Task Start(CancellationToken cancellationToken = default)
26+
{
27+
var webSocket = new ClientWebSocket();
28+
await webSocket.ConnectAsync(new Uri($@"ws://localhost:{port}/"), cancellationToken);
29+
channel = new WebSocketChannel(webSocket);
30+
channel.MessageReceived += msg => MessageReceived?.Invoke(msg);
31+
channel.ClosedPrematurely += () => Closed?.Invoke();
32+
channel.Start(cancellationToken);
33+
}
34+
35+
public async Task SendAsync(string message)
36+
{
37+
if (channel == null)
38+
throw new InvalidOperationException($@"Must {nameof(Start)} first.");
39+
40+
await channel.SendAsync(message);
41+
}
42+
43+
public async Task StopAsync(CancellationToken stoppingToken = default)
44+
{
45+
try
46+
{
47+
if (channel != null)
48+
await channel.StopAsync(stoppingToken).ConfigureAwait(false);
49+
}
50+
catch (OperationCanceledException)
51+
{
52+
// has to be caught manually because outer task isn't accepting `stoppingToken`.
53+
}
54+
}
55+
56+
public void Dispose()
57+
{
58+
channel?.Dispose();
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)