Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
13 changes: 0 additions & 13 deletions osu.Desktop/IPC/Messages/HitCountMessage.cs

This file was deleted.

56 changes: 24 additions & 32 deletions osu.Desktop/IPC/OsuWebSocketProvider.cs
Comment thread
bdach marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,55 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using osu.Desktop.IPC.Messages;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.IPC;
using osu.Game.IPC.Messages;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using JsonConvert = Newtonsoft.Json.JsonConvert;

namespace osu.Desktop.IPC
{
public partial class OsuWebSocketProvider : Component
public partial class OsuWebSocketProvider : Component, IWebSocketProvider
{
private WebSocketServer? server;
private readonly Bindable<ScoreInfo> lastLocalScore = new Bindable<ScoreInfo>();
private readonly List<WebSocketDataSource> dataSources = [];

[BackgroundDependencyLoader]
private void load(SessionStatics sessionStatics)
private void load()
{
server = new WebSocketServer(49727);
server.StartAsync().FireAndForget(onError: ex => Logger.Error(ex, "Failed to start websocket"));

sessionStatics.BindWith(Static.LastLocalUserScore, lastLocalScore);
}

protected override void LoadComplete()
public void Register(WebSocketDataSource dataSource)
{
base.LoadComplete();

lastLocalScore.BindValueChanged(val =>
{
if (val.NewValue == null)
return;

if (server?.IsRunning != true)
return;

var msg = new HitCountMessage { NewHits = val.NewValue.Statistics.Where(kv => kv.Key.IsBasic() && kv.Key.IsHit()).Sum(kv => kv.Value) };
broadcast(msg);
});
dataSources.Add(dataSource);
dataSource.MessageReceived += onDataSourceMessageReceived;
}

private void broadcast(OsuWebSocketMessage message)
public void Unregister(WebSocketDataSource dataSource)
{
if (server?.IsRunning != true)
return;

string messageString = JsonConvert.SerializeObject(message);
server.BroadcastAsync(messageString).FireAndForget();
dataSource.MessageReceived -= onDataSourceMessageReceived;
dataSources.Remove(dataSource);
}

private void onDataSourceMessageReceived(OsuWebSocketMessage message) =>
Task.Run(async () =>
{
if (server?.IsRunning != true)
return;

string messageString = JsonSerializer.Serialize(message, message.GetType());
await server.BroadcastAsync(messageString).ConfigureAwait(false);
})
.FireAndForget();

protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Expand Down
11 changes: 8 additions & 3 deletions osu.Desktop/OsuGameDesktop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ public override bool RestartAppWhenExited()

protected override void LoadComplete()
{
// this is done before `base.LoadComplete` so that the game can immediately register data sources.
if (EnableWebSocketServer)
{
var provider = new OsuWebSocketProvider();
Add(provider);
Dependencies.CacheAs<IWebSocketProvider>(provider);
}

base.LoadComplete();

LoadComponentAsync(new DiscordRichPresence(), Add);
Expand All @@ -151,9 +159,6 @@ protected override void LoadComplete()

osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);

if (EnableWebSocketServer)
Add(new OsuWebSocketProvider());
}

public override void SetHost(GameHost host)
Expand Down
151 changes: 151 additions & 0 deletions osu.Game/IPC/DataSources/BeatmapStateWebSocketDataSource.cs
Comment thread
bdach marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.IPC.Messages;
using osu.Game.IPC.Models;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;

namespace osu.Game.IPC.DataSources
{
public partial class BeatmapStateWebSocketDataSource : WebSocketDataSource
{
[Resolved]
private Bindable<WorkingBeatmap> workingBeatmap { get; set; } = null!;

[Resolved]
private IBindable<RulesetInfo> rulesetInfo { get; set; } = null!;

[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;

[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;

private ModSettingChangeTracker? modSettingChangeTracker;
private ScheduledDelegate? debouncedModSettingsChange;

public BeatmapStateWebSocketDataSource(IWebSocketProvider provider)
: base(provider) { }

protected override void LoadComplete()
{
base.LoadComplete();

workingBeatmap.BindValueChanged(val =>
{
if (val.NewValue.BeatmapInfo.OnlineID == val.OldValue.BeatmapInfo.OnlineID)
return;

updatePlayerState().FireAndForget();
});

rulesetInfo.BindValueChanged(val =>
{
if (val.NewValue.Equals(val.OldValue))
return;

updatePlayerState().FireAndForget();
});

mods.BindValueChanged(val =>
{
if (val.OldValue.SequenceEqual(val.NewValue, ReferenceEqualityComparer.Instance))
return;

modSettingChangeTracker?.Dispose();

updatePlayerState().FireAndForget();

modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
modSettingChangeTracker.SettingChanged += _ =>
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = Scheduler.AddDelayed(() => updatePlayerState().FireAndForget(), 100);
};
});
}

private async Task updatePlayerState()
{
if (workingBeatmap.Value is DummyWorkingBeatmap)
return;

double rate = ModUtils.CalculateRateWithMods(mods.Value);

var ruleset = rulesetInfo.Value.CreateInstance();
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(workingBeatmap.Value.BeatmapInfo, mods.Value);

var starDifficulty = await difficultyCache.GetDifficultyAsync(workingBeatmap.Value.BeatmapInfo, rulesetInfo.Value, mods.Value).ConfigureAwait(false);

var msg = new BeatmapStateWebSocketMessage
{
Beatmap = new WebSocketBeatmap
{
BeatmapId = workingBeatmap.Value.BeatmapInfo.OnlineID,
BeatmapSetId = workingBeatmap.Value.BeatmapSetInfo.OnlineID,
BeatmapHash = workingBeatmap.Value.BeatmapInfo.OnlineMD5Hash,
Metadata = new WebSocketBeatmapMetadata
{
Artist = workingBeatmap.Value.BeatmapInfo.Metadata.Artist,
ArtistUnicode = workingBeatmap.Value.BeatmapInfo.Metadata.ArtistUnicode,
Title = workingBeatmap.Value.BeatmapInfo.Metadata.Title,
TitleUnicode = workingBeatmap.Value.BeatmapInfo.Metadata.TitleUnicode,
Author = workingBeatmap.Value.BeatmapInfo.Metadata.Author.Username,
Source = workingBeatmap.Value.BeatmapInfo.Metadata.Source,
Tags = workingBeatmap.Value.BeatmapInfo.Metadata.Tags,
UserTags = workingBeatmap.Value.BeatmapInfo.Metadata.UserTags.ToArray(),
},
Difficulty = new WebSocketBeatmapDifficulty
{
ApproachRate = Math.Round(adjustedDifficulty.ApproachRate, 2),
CircleSize = Math.Round(adjustedDifficulty.CircleSize, 2),
DrainRate = Math.Round(adjustedDifficulty.DrainRate, 2),
OverallDifficulty = Math.Round(adjustedDifficulty.OverallDifficulty, 2),
},
DifficultyName = workingBeatmap.Value.BeatmapInfo.DifficultyName,
RulesetId = workingBeatmap.Value.BeatmapInfo.Ruleset.OnlineID,
BPM = FormatUtils.RoundBPM(workingBeatmap.Value.BeatmapInfo.BPM, rate),
StarRating = starDifficulty?.Stars.FloorToDecimalDigits(2) ?? workingBeatmap.Value.BeatmapInfo.StarRating.FloorToDecimalDigits(2),
MaximumPP = Math.Round(starDifficulty?.PerformanceAttributes?.Total ?? 0, 2),
MaxCombo = starDifficulty?.MaxCombo ?? 0,
Status = workingBeatmap.Value.BeatmapInfo.Status,
TotalLength = (int)Math.Round(workingBeatmap.Value.BeatmapInfo.Length / rate),
DrainLength = (int)Math.Round(workingBeatmap.Value.Beatmap.CalculateDrainLength() / rate),
ObjectCount = workingBeatmap.Value.BeatmapInfo.TotalObjectCount,
},
RulesetId = rulesetInfo.Value.OnlineID,
Mods = mods.Value.Select(modToWebSocketMod).ToArray(),
};

BroadcastMessage(msg);
}

private static WebSocketMod modToWebSocketMod(Mod mod)
{
var settings = new Dictionary<string, object>();

foreach (var (_, property) in mod.GetSettingsSourceProperties())
{
var bindable = (IBindable)property.GetValue(mod)!;

if (!bindable.IsDefault)
settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue());
}

return new WebSocketMod { Acronym = mod.Acronym, Settings = settings };
}
}
}
71 changes: 71 additions & 0 deletions osu.Game/IPC/DataSources/UserActivityWebSocketDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.IPC.Messages;
using osu.Game.IPC.Models;
using osu.Game.Users;

namespace osu.Game.IPC.DataSources
{
public partial class UserActivityWebSocketDataSource : WebSocketDataSource
{
private readonly Bindable<UserActivity?> userActivity = new Bindable<UserActivity?>();

public UserActivityWebSocketDataSource(IWebSocketProvider provider)
: base(provider) { }

[BackgroundDependencyLoader]
private void load(SessionStatics sessionStatics)
{
sessionStatics.BindWith(Static.UserOnlineActivity, userActivity);
}

protected override void LoadComplete()
{
base.LoadComplete();

userActivity.BindValueChanged(onUserActivityChange);
}

private void onUserActivityChange(ValueChangedEvent<UserActivity?> change)
{
if (change.NewValue == null)
return;

var msg = new UserActivityWebSocketMessage
{
Status = change.NewValue.GetType().Name,
Comment thread
bdach marked this conversation as resolved.
Data = getUserActivityData(change.NewValue),
};

BroadcastMessage(msg);
}

private static object? getUserActivityData(UserActivity userActivity)
{
switch (userActivity)
{
case UserActivity.InLobby inLobby:
return new WebSocketInLobbyUserActivityData
{
RoomId = inLobby.RoomID,
RoomName = inLobby.RoomName,
};

case UserActivity.WatchingReplay watchingReplay:
return new WebSocketWatchingReplayUserActivityData
{
ScoreId = watchingReplay.ScoreID,
UserId = watchingReplay.UserID,
BeatmapId = watchingReplay.BeatmapID,
};

default:
return null;
}
}
}
}
14 changes: 14 additions & 0 deletions osu.Game/IPC/IWebSocketProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using osu.Framework.Allocation;

namespace osu.Game.IPC
{
[Cached]
public interface IWebSocketProvider
{
void Register(WebSocketDataSource dataSource);
void Unregister(WebSocketDataSource dataSource);
}
}
20 changes: 20 additions & 0 deletions osu.Game/IPC/Messages/BeatmapStateWebSocketMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Text.Json.Serialization;
using osu.Game.IPC.Models;

namespace osu.Game.IPC.Messages
{
public class BeatmapStateWebSocketMessage : OsuWebSocketMessage
{
[JsonPropertyName("beatmap")]
public required WebSocketBeatmap Beatmap { get; init; }

[JsonPropertyName("ruleset_id")]
public required int RulesetId { get; init; }

[JsonPropertyName("mods")]
public required WebSocketMod[] Mods { get; init; }
}
}
Loading
Loading