Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
4 changes: 2 additions & 2 deletions osu.Desktop/IPC/Messages/OsuWebSocketMessage.cs
Original file line number Diff line number Diff line change
@@ -1,14 +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 Newtonsoft.Json;
using System.Text.Json.Serialization;
using osu.Framework.Extensions.TypeExtensions;

namespace osu.Desktop.IPC.Messages
{
public abstract class OsuWebSocketMessage
{
[JsonProperty("type")]
[JsonPropertyName("type")]
public string Type { get; }

protected OsuWebSocketMessage()
Expand Down
100 changes: 100 additions & 0 deletions osu.Desktop/IPC/Messages/PlayerStateMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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.Beatmaps;
using osu.Game.Online.API;

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

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

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

public class Beatmap
Comment thread
tsunyoku marked this conversation as resolved.
Outdated
{
[JsonPropertyName("beatmap_id")]
public required int BeatmapId { get; init; }

[JsonPropertyName("beatmapset_id")]
public required int BeatmapSetId { get; init; }

[JsonPropertyName("beatmap_hash")]
public required string BeatmapHash { get; init; }

[JsonPropertyName("metadata")]
public required BeatmapMetadata Metadata { get; init; }

[JsonPropertyName("difficulty")]
public required BeatmapDifficulty Difficulty { get; init; }

[JsonPropertyName("difficulty_name")]
public required string DifficultyName { get; init; }

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

[JsonPropertyName("bpm")]
public required double BPM { get; init; }

[JsonPropertyName("star_rating")]
public required double StarRating { get; init; }

[JsonPropertyName("max_combo")]
public required int MaxCombo { get; init; }

[JsonPropertyName("status")]
[JsonConverter(typeof(JsonStringEnumConverter<BeatmapOnlineStatus>))]
public required BeatmapOnlineStatus Status { get; init; }
Comment thread
tsunyoku marked this conversation as resolved.
Outdated
}

public class BeatmapMetadata
{
[JsonPropertyName("artist")]
public required string Artist { get; init; }

[JsonPropertyName("artist_unicode")]
public required string ArtistUnicode { get; init; }

[JsonPropertyName("title")]
public required string Title { get; init; }

[JsonPropertyName("title_unicode")]
public required string TitleUnicode { get; init; }

[JsonPropertyName("author")]
public required string Author { get; init; }

[JsonPropertyName("source")]
public required string Source { get; init; }

[JsonPropertyName("tags")]
public required string Tags { get; init; }

[JsonPropertyName("user_tags")]
public required string[] UserTags { get; init; }
}

public class BeatmapDifficulty
{
[JsonPropertyName("approach_rate")]
public required double ApproachRate { get; init; }

[JsonPropertyName("circle_size")]
public required double CircleSize { get; init; }

[JsonPropertyName("overall_difficulty")]
public required double OverallDifficulty { get; init; }

[JsonPropertyName("drain_rate")]
public required double DrainRate { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// 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.Text.Json.Serialization;

namespace osu.Desktop.IPC.Messages
{
public class HitCountMessage : OsuWebSocketMessage
public class UserActivityMessage : OsuWebSocketMessage
{
[JsonProperty("new_hits")]
public long NewHits { get; init; }
[JsonPropertyName("status")]
public required string Status { get; init; }
}
}
144 changes: 133 additions & 11 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,180 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.IPC.Messages;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.IPC;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using JsonConvert = Newtonsoft.Json.JsonConvert;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Users;
using osu.Game.Utils;
using Beatmap = osu.Desktop.IPC.Messages.Beatmap;
using BeatmapDifficulty = osu.Desktop.IPC.Messages.BeatmapDifficulty;
using BeatmapMetadata = osu.Desktop.IPC.Messages.BeatmapMetadata;

namespace osu.Desktop.IPC
{
public partial class OsuWebSocketProvider : Component
{
private WebSocketServer? server;
private readonly Bindable<ScoreInfo> lastLocalScore = new Bindable<ScoreInfo>();
private readonly Bindable<UserActivity?> userActivity = new Bindable<UserActivity?>();

[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;

private readonly object modSettingsLock = new object();

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

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

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

lastLocalScore.BindValueChanged(val =>
userActivity.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);
var msg = new UserActivityMessage
{
Status = val.NewValue.GetType().Name,
};

broadcast(msg).FireAndForget();
}, true);

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

updatePlayerState().FireAndForget();
});

rulesetInfo.BindValueChanged(_ => updatePlayerState().FireAndForget());

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

updatePlayerState().FireAndForget();

modSettingChangeTracker?.Dispose();

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

updatePlayerState().FireAndForget();
}

private void broadcast(OsuWebSocketMessage message)
private async Task updatePlayerState()
{
if (workingBeatmap.Value is DummyWorkingBeatmap)
return;

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

string messageString = JsonConvert.SerializeObject(message);
server.BroadcastAsync(messageString).FireAndForget();
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 PlayerStateMessage
{
Beatmap = new Beatmap
{
BeatmapId = workingBeatmap.Value.BeatmapInfo.OnlineID,
BeatmapSetId = workingBeatmap.Value.BeatmapSetInfo.OnlineID,
BeatmapHash = workingBeatmap.Value.BeatmapInfo.OnlineMD5Hash,
Metadata = new BeatmapMetadata
{
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 BeatmapDifficulty
{
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),
MaxCombo = starDifficulty?.MaxCombo ?? 0,
Status = workingBeatmap.Value.BeatmapInfo.Status,
},
RulesetId = rulesetInfo.Value.OnlineID,
Mods = mods.Value.Select(m => new APIMod(m)).ToArray(),
};

await broadcast(msg).ConfigureAwait(false);
}

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

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

protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Expand All @@ -70,6 +187,11 @@ protected override void Dispose(bool isDisposing)
server.StopAsync(cts.Token).WaitSafely();
server = null;
}

modSettingChangeTracker?.Dispose();

debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = null;
}
}
}
5 changes: 4 additions & 1 deletion osu.Game/Online/API/APIMod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using MessagePack;
using Newtonsoft.Json;
using osu.Framework.Bindables;
Expand All @@ -19,15 +20,17 @@ namespace osu.Game.Online.API
public class APIMod : IEquatable<APIMod>
{
[JsonProperty("acronym")]
[JsonPropertyName("acronym")]
[Key(0)]
public string Acronym { get; set; } = string.Empty;

[JsonProperty("settings")]
[JsonPropertyName("settings")]
[Key(1)]
[MessagePackFormatter(typeof(ModSettingsDictionaryFormatter))]
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();

[JsonConstructor]
[Newtonsoft.Json.JsonConstructor]
Comment thread
tsunyoku marked this conversation as resolved.
Outdated
[SerializationConstructor]
public APIMod()
{
Expand Down
Loading