Skip to content
Open
Show file tree
Hide file tree
Changes from 18 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
148 changes: 148 additions & 0 deletions osu.Game/IPC/DataSources/PlayerStateWebSocketDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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 PlayerStateWebSocketDataSource : 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 PlayerStateWebSocketDataSource(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 PlayerStateWebSocketMessage
{
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,
},
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 };
}
}
}
84 changes: 84 additions & 0 deletions osu.Game/IPC/DataSources/UserActivityWebSocketDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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.InGame inGame:
return new WebSocketInGameUserActivityData
{
BeatmapId = inGame.BeatmapID,
RulesetId = inGame.RulesetID,
};

case UserActivity.InLobby inLobby:
return new WebSocketInLobbyUserActivityData
{
RoomId = inLobby.RoomID,
RoomName = inLobby.RoomName,
};

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

case UserActivity.EditingBeatmap editingBeatmap:
return new WebSocketEditingBeatmapUserActivityData
{
BeatmapId = editingBeatmap.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);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// 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 Newtonsoft.Json;
using System;
using System.Text.Json.Serialization;
using osu.Framework.Extensions.TypeExtensions;

namespace osu.Desktop.IPC.Messages
namespace osu.Game.IPC.Messages
{
[Serializable]
public abstract class OsuWebSocketMessage
Comment thread
tsunyoku marked this conversation as resolved.
{
[JsonProperty("type")]
[JsonPropertyName("type")]
public string Type { get; }

protected OsuWebSocketMessage()
Expand Down
Loading
Loading