From 14a499cf5b6b2501a88e540cc2a5ebd7786fbed2 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Tue, 26 Mar 2024 20:00:26 +0100 Subject: [PATCH 01/75] wip(1.35): initial refactor and cleanup --- MultiplayerCore.sln.DotSettings | 3 + .../Beatmaps/Abstractions/DifficultyColors.cs | 112 ------------ .../Beatmaps/Abstractions/MpBeatmap.cs | 36 ++++ .../Beatmaps/Abstractions/MpBeatmapLevel.cs | 49 ----- .../Beatmaps/BeatSaverBeatmapLevel.cs | 32 ++-- MultiplayerCore/Beatmaps/LocalBeatmapLevel.cs | 53 +++--- .../Beatmaps/NetworkBeatmapLevel.cs | 45 +++-- .../Beatmaps/NoInfoBeatmapLevel.cs | 17 +- .../Beatmaps/Packets/MpBeatmapPacket.cs | 44 ++--- .../Providers/MpBeatmapLevelProvider.cs | 25 +-- .../Beatmaps/Serializable/DifficultyColors.cs | 111 ++++++++++++ MultiplayerCore/MultiplayerCore.csproj | 18 +- .../Objects/MpEntitlementChecker.cs | 10 +- MultiplayerCore/Objects/MpLevelDownloader.cs | 14 +- MultiplayerCore/Objects/MpLevelLoader.cs | 157 +++++++++++----- MultiplayerCore/Objects/MpPlayersDataModel.cs | 80 ++++---- .../Patchers/CustomLevelsPatcher.cs | 17 -- MultiplayerCore/Patchers/UpdateMapPatcher.cs | 171 +++++++++--------- MultiplayerCore/Plugin.cs | 9 +- MultiplayerCore/UI/MpColorsUI.cs | 16 +- MultiplayerCore/UI/MpLoadingIndicator.cs | 13 +- MultiplayerCore/UI/MpRequirementsUI.cs | 159 ++++++++-------- MultiplayerCore/Utilities.cs | 6 +- MultiplayerCore/manifest.json | 16 +- 24 files changed, 629 insertions(+), 584 deletions(-) create mode 100644 MultiplayerCore.sln.DotSettings delete mode 100644 MultiplayerCore/Beatmaps/Abstractions/DifficultyColors.cs create mode 100644 MultiplayerCore/Beatmaps/Abstractions/MpBeatmap.cs delete mode 100644 MultiplayerCore/Beatmaps/Abstractions/MpBeatmapLevel.cs create mode 100644 MultiplayerCore/Beatmaps/Serializable/DifficultyColors.cs diff --git a/MultiplayerCore.sln.DotSettings b/MultiplayerCore.sln.DotSettings new file mode 100644 index 0000000..7f1f85c --- /dev/null +++ b/MultiplayerCore.sln.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/MultiplayerCore/Beatmaps/Abstractions/DifficultyColors.cs b/MultiplayerCore/Beatmaps/Abstractions/DifficultyColors.cs deleted file mode 100644 index a19b1c0..0000000 --- a/MultiplayerCore/Beatmaps/Abstractions/DifficultyColors.cs +++ /dev/null @@ -1,112 +0,0 @@ -using LiteNetLib.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static SongCore.Data.ExtraSongData; - -namespace MultiplayerCore.Beatmaps.Abstractions -{ - public class DifficultyColors : INetSerializable - { - public bool AnyAreNotNull => _colorLeft != null || _colorRight != null || _envColorLeft != null || _envColorRight != null || _envColorLeftBoost != null || _envColorRightBoost != null || _obstacleColor != null; - - public MapColor? _colorLeft; - public MapColor? _colorRight; - public MapColor? _envColorLeft; - public MapColor? _envColorRight; - public MapColor? _envColorLeftBoost; - public MapColor? _envColorRightBoost; - public MapColor? _obstacleColor; - - public DifficultyColors() { } - - public DifficultyColors(MapColor? colorLeft, MapColor? colorRight, MapColor? envColorLeft, MapColor? envColorRight, MapColor? envColorLeftBoost, MapColor? envColorRightBoost, MapColor? obstacleColor) - { - _colorLeft = colorLeft; - _colorRight = colorRight; - _envColorLeft = envColorLeft; - _envColorRight = envColorRight; - _envColorLeftBoost = envColorLeftBoost; - _envColorRightBoost = envColorRightBoost; - } - - public void Serialize(NetDataWriter writer) - { - byte colors = (byte)(_colorLeft != null ? 1 : 0); - colors |= (byte)((_colorRight != null ? 1 : 0) << 1); - colors |= (byte)((_envColorLeft != null ? 1 : 0) << 2); - colors |= (byte)((_envColorRight != null ? 1 : 0) << 3); - colors |= (byte)((_envColorLeftBoost != null ? 1 : 0) << 4); - colors |= (byte)((_envColorRightBoost != null ? 1 : 0) << 5); - colors |= (byte)((_obstacleColor != null ? 1 : 0) << 6); - writer.Put(colors); - - if (_colorLeft != null) - ((MapColorSerializable)_colorLeft).Serialize(writer); - if (_colorRight != null) - ((MapColorSerializable)_colorRight).Serialize(writer); - if (_envColorLeft != null) - ((MapColorSerializable)_envColorLeft).Serialize(writer); - if (_envColorRight != null) - ((MapColorSerializable)_envColorRight).Serialize(writer); - if (_envColorLeftBoost != null) - ((MapColorSerializable)_envColorLeftBoost).Serialize(writer); - if (_envColorRightBoost != null) - ((MapColorSerializable)_envColorRightBoost).Serialize(writer); - if (_obstacleColor != null) - ((MapColorSerializable)_obstacleColor).Serialize(writer); - } - - public void Deserialize(NetDataReader reader) - { - var colors = reader.GetByte(); - if ((colors & 0x1) != 0) - _colorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); - if (((colors >> 1) & 0x1) != 0) - _colorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); - if (((colors >> 2) & 0x1) != 0) - _colorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); - if (((colors >> 3) & 0x1) != 0) - _colorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); - if (((colors >> 4) & 0x1) != 0) - _colorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); - if (((colors >> 5) & 0x1) != 0) - _colorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); - if (((colors >> 6) & 0x1) != 0) - _colorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); - } - - public class MapColorSerializable : INetSerializable - { - public float r; - public float g; - public float b; - - public MapColorSerializable(float red, float green, float blue) - { - r = red; - g = green; - b = blue; - } - - public void Serialize(NetDataWriter writer) - { - writer.Put(r); - writer.Put(g); - writer.Put(b); - } - - public void Deserialize(NetDataReader reader) - { - r = reader.GetFloat(); - g = reader.GetFloat(); - b = reader.GetFloat(); - } - - public static implicit operator MapColor(MapColorSerializable c) => new MapColor(c.r, c.g, c.b); - public static explicit operator MapColorSerializable(MapColor c) => new MapColorSerializable(c.r, c.g, c.b); - } - } -} diff --git a/MultiplayerCore/Beatmaps/Abstractions/MpBeatmap.cs b/MultiplayerCore/Beatmaps/Abstractions/MpBeatmap.cs new file mode 100644 index 0000000..47443a5 --- /dev/null +++ b/MultiplayerCore/Beatmaps/Abstractions/MpBeatmap.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MultiplayerCore.Beatmaps.Serializable; +using UnityEngine; +using static SongCore.Data.ExtraSongData; + +namespace MultiplayerCore.Beatmaps.Abstractions +{ + /// + /// Base class for Beatmap data that can be used in multiplayer. + /// + public abstract class MpBeatmap + { + /// + /// The hash of the level. Should be the same on all clients. + /// + public abstract string LevelHash { get; protected set; } + /// + /// The local ID of the level. Can vary between clients. + /// + public string LevelID => $"custom_level_{LevelHash}"; + public abstract string SongName { get; } + public abstract string SongSubName { get; } + public abstract string SongAuthorName { get; } + public abstract string LevelAuthorName { get; } + public virtual float BeatsPerMinute { get; protected set; } + public virtual float SongDuration { get; protected set; } + public virtual Dictionary> Requirements { get; protected set; } = new(); + public virtual Dictionary> DifficultyColors { get; protected set; } = new(); + public virtual Contributor[]? Contributors { get; protected set; } = null!; + + public virtual Task TryGetCoverSpriteAsync(CancellationToken cancellationToken) + => Task.FromResult(null!); + } +} \ No newline at end of file diff --git a/MultiplayerCore/Beatmaps/Abstractions/MpBeatmapLevel.cs b/MultiplayerCore/Beatmaps/Abstractions/MpBeatmapLevel.cs deleted file mode 100644 index fb500e8..0000000 --- a/MultiplayerCore/Beatmaps/Abstractions/MpBeatmapLevel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using UnityEngine; -using static SongCore.Data.ExtraSongData; - -namespace MultiplayerCore.Beatmaps.Abstractions -{ - public abstract class MpBeatmapLevel : IPreviewBeatmapLevel - { - /// - /// The local ID of the level. Can vary between clients. - /// - public virtual string levelID => $"custom_level_{levelHash}"; - - /// - /// The hash of the level. Should be the same on all clients. - /// - public abstract string levelHash { get; protected set; } - - public abstract string songName { get; } - public abstract string songSubName { get; } - public abstract string songAuthorName { get; } - public abstract string levelAuthorName { get; } - - public virtual float beatsPerMinute { get; protected set; } - public virtual float songDuration { get; protected set; } - public virtual float previewStartTime { get; protected set; } - public virtual float previewDuration { get; protected set; } - public virtual EnvironmentInfoSO[] environmentInfos { get; private set; } - public virtual IReadOnlyList? previewDifficultyBeatmapSets { get; protected set; } - - public virtual float songTimeOffset { get; protected set; } // Not needed - public float shuffle { get; private set; } // Not needed - public float shufflePeriod { get; private set; } // Not needed - public EnvironmentInfoSO? environmentInfo => null; // Not needed, used for level load - public EnvironmentInfoSO? allDirectionsEnvironmentInfo => null; // Not needed, used for level load - - // SongCore stuff - public virtual Dictionary> requirements { get; protected set; } = new(); - public virtual Dictionary> difficultyColors { get; protected set; } = new(); - public virtual Contributor[]? contributors { get; protected set; } = null!; - - public virtual PlayerSensitivityFlag contentRating { get; protected set; } = PlayerSensitivityFlag.Unknown; - - public virtual Task GetCoverImageAsync(CancellationToken cancellationToken) - => Task.FromResult(null!); - } -} diff --git a/MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs b/MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs index f9c50c4..f4d9748 100644 --- a/MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs +++ b/MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs @@ -1,10 +1,10 @@ -using BeatSaverSharp.Models; -using MultiplayerCore.Beatmaps.Abstractions; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BeatSaverSharp.Models; +using MultiplayerCore.Beatmaps.Abstractions; using UnityEngine; using static BeatSaverSharp.Models.BeatmapDifficulty; using static SongCore.Data.ExtraSongData; @@ -12,20 +12,20 @@ namespace MultiplayerCore.Beatmaps { /// - /// An created from data from BeatSaver. + /// Beatmap level data that was loaded remotely from the BeatSaver API. /// - public class BeatSaverBeatmapLevel : MpBeatmapLevel + public class BeatSaverBeatmapLevel : MpBeatmap { - public override string levelHash { get; protected set; } + public override string LevelHash { get; protected set; } - public override string songName => _beatmap.Metadata.SongName; - public override string songSubName => _beatmap.Metadata.SongSubName; - public override string songAuthorName => _beatmap.Metadata.SongAuthorName; - public override string levelAuthorName => _beatmap.Metadata.LevelAuthorName; - public override float beatsPerMinute => _beatmap.Metadata.BPM; - public override float songDuration => _beatmap.Metadata.Duration; + public override string SongName => _beatmap.Metadata.SongName; + public override string SongSubName => _beatmap.Metadata.SongSubName; + public override string SongAuthorName => _beatmap.Metadata.SongAuthorName; + public override string LevelAuthorName => _beatmap.Metadata.LevelAuthorName; + public override float BeatsPerMinute => _beatmap.Metadata.BPM; + public override float SongDuration => _beatmap.Metadata.Duration; - public override Dictionary> requirements + public override Dictionary> Requirements { get { @@ -58,7 +58,7 @@ public override Dictionary> requ } } - public override Contributor[] contributors => new Contributor[] { new Contributor + public override Contributor[] Contributors => new Contributor[] { new Contributor { _role = "Uploader", _name = _beatmap.Uploader.Name, @@ -69,11 +69,11 @@ public override Dictionary> requ public BeatSaverBeatmapLevel(string hash, Beatmap beatmap) { - levelHash = hash; + LevelHash = hash; _beatmap = beatmap; } - public override async Task GetCoverImageAsync(CancellationToken cancellationToken) + public override async Task TryGetCoverSpriteAsync(CancellationToken cancellationToken) { byte[]? coverBytes = await _beatmap.LatestVersion.DownloadCoverImage(cancellationToken); if (coverBytes == null || coverBytes.Length == 0) diff --git a/MultiplayerCore/Beatmaps/LocalBeatmapLevel.cs b/MultiplayerCore/Beatmaps/LocalBeatmapLevel.cs index 585d02f..fe0790d 100644 --- a/MultiplayerCore/Beatmaps/LocalBeatmapLevel.cs +++ b/MultiplayerCore/Beatmaps/LocalBeatmapLevel.cs @@ -1,39 +1,34 @@ -using MultiplayerCore.Beatmaps.Abstractions; -using SongCore.Data; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MultiplayerCore.Beatmaps.Abstractions; +using MultiplayerCore.Beatmaps.Serializable; using UnityEngine; using static SongCore.Data.ExtraSongData; namespace MultiplayerCore.Beatmaps { /// - /// An created from a local preview. + /// Beatmap level data that was loaded locally by SongCore. /// - public class LocalBeatmapLevel : MpBeatmapLevel + public class LocalBeatmapLevel : MpBeatmap { - public override string levelHash { get; protected set; } + public override string LevelHash { get; protected set; } - public override string songName => _preview.songName; - public override string songSubName => _preview.songSubName; - public override string songAuthorName => _preview.songAuthorName; - public override string levelAuthorName => _preview.levelAuthorName; + public override string SongName => _localBeatmapLevel.songName; + public override string SongSubName => _localBeatmapLevel.songSubName; + public override string SongAuthorName => _localBeatmapLevel.songAuthorName; + public override string LevelAuthorName => string.Join(", ", _localBeatmapLevel.allMappers); - public override float beatsPerMinute => _preview.beatsPerMinute; - public override float songDuration => _preview.songDuration; - public override float previewStartTime => _preview.previewStartTime; - public override float previewDuration => _preview.previewDuration; - public override EnvironmentInfoSO[] environmentInfos => _preview.environmentInfos; - public override IReadOnlyList? previewDifficultyBeatmapSets => _preview.previewDifficultyBeatmapSets; - - public override Dictionary> requirements + public override float BeatsPerMinute => _localBeatmapLevel.beatsPerMinute; + public override float SongDuration => _localBeatmapLevel.songDuration; + + public override Dictionary> Requirements { get { Dictionary> reqs = new(); - var difficulties = SongCore.Collections.RetrieveExtraSongData(levelHash)?._difficulties; + var difficulties = SongCore.Collections.RetrieveExtraSongData(LevelHash)?._difficulties; if (difficulties == null) return new(); foreach (var difficulty in difficulties) @@ -46,12 +41,12 @@ public override Dictionary> requ } } - public override Dictionary> difficultyColors + public override Dictionary> DifficultyColors { get { Dictionary> colors = new(); - var difficulties = SongCore.Collections.RetrieveExtraSongData(levelHash)?._difficulties; + var difficulties = SongCore.Collections.RetrieveExtraSongData(LevelHash)?._difficulties; if (difficulties == null) return new(); foreach (var difficulty in difficulties) @@ -65,17 +60,17 @@ public override Dictionary SongCore.Collections.RetrieveExtraSongData(levelHash)?.contributors ?? new Contributor[0]; + public override Contributor[] Contributors => SongCore.Collections.RetrieveExtraSongData(LevelHash)?.contributors ?? new Contributor[0]; - private readonly IPreviewBeatmapLevel _preview; + private readonly BeatmapLevel _localBeatmapLevel; - public LocalBeatmapLevel(string hash, IPreviewBeatmapLevel preview) + public LocalBeatmapLevel(string hash, BeatmapLevel localBeatmapLevel) { - levelHash = hash; - _preview = preview; + LevelHash = hash; + _localBeatmapLevel = localBeatmapLevel; } - public override Task GetCoverImageAsync(CancellationToken cancellationToken) - => _preview.GetCoverImageAsync(cancellationToken); + public override Task TryGetCoverSpriteAsync(CancellationToken cancellationToken) + => _localBeatmapLevel.previewMediaData.GetCoverSpriteAsync(cancellationToken); } } diff --git a/MultiplayerCore/Beatmaps/NetworkBeatmapLevel.cs b/MultiplayerCore/Beatmaps/NetworkBeatmapLevel.cs index a2d59e3..033943d 100644 --- a/MultiplayerCore/Beatmaps/NetworkBeatmapLevel.cs +++ b/MultiplayerCore/Beatmaps/NetworkBeatmapLevel.cs @@ -1,51 +1,56 @@ -using BeatSaverSharp; -using MultiplayerCore.Beatmaps.Abstractions; -using MultiplayerCore.Beatmaps.Packets; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using BeatSaverSharp; +using MultiplayerCore.Beatmaps.Abstractions; +using MultiplayerCore.Beatmaps.Packets; +using MultiplayerCore.Beatmaps.Serializable; using UnityEngine; using static SongCore.Data.ExtraSongData; namespace MultiplayerCore.Beatmaps { /// - /// An created from data from a network player. + /// Beatmap level data based on an MpBeatmapPacket from another player. /// - class NetworkBeatmapLevel : MpBeatmapLevel + class NetworkBeatmapLevel : MpBeatmap { - public override string levelHash { get; protected set; } + public override string LevelHash { get; protected set; } + + public override string SongName => _packet.songName; + public override string SongSubName => _packet.songSubName; + public override string SongAuthorName => _packet.songAuthorName; + public override string LevelAuthorName => _packet.levelAuthorName; + public override float BeatsPerMinute => _packet.beatsPerMinute; + public override float SongDuration => _packet.songDuration; + + public override Dictionary> Requirements => + new() { { _packet.characteristicName, _packet.requirements } }; - public override string songName => _packet.songName; - public override string songSubName => _packet.songSubName; - public override string songAuthorName => _packet.songAuthorName; - public override string levelAuthorName => _packet.levelAuthorName; - public override float beatsPerMinute => _packet.beatsPerMinute; - public override float songDuration => _packet.songDuration; + public override Dictionary> DifficultyColors => + new() { { _packet.characteristicName, _packet.mapColors } }; - public override Dictionary> requirements => new() { { _packet.characteristic, _packet.requirements } }; - public override Dictionary> difficultyColors => new() { { _packet.characteristic, _packet.mapColors } }; - public override Contributor[] contributors => _packet.contributors; + public override Contributor[] Contributors => _packet.contributors; private readonly MpBeatmapPacket _packet; private readonly BeatSaver? _beatsaver; public NetworkBeatmapLevel(MpBeatmapPacket packet) { - levelHash = packet.levelHash; + LevelHash = packet.levelHash; _packet = packet; } public NetworkBeatmapLevel(MpBeatmapPacket packet, BeatSaver beatsaver) { - levelHash = packet.levelHash; + LevelHash = packet.levelHash; _packet = packet; _beatsaver = beatsaver; } private Task? _coverImageTask; - public override Task GetCoverImageAsync(CancellationToken cancellationToken) + public override Task TryGetCoverSpriteAsync(CancellationToken cancellationToken) { if (_coverImageTask == null) _coverImageTask = FetchCoverImage(cancellationToken); @@ -56,7 +61,7 @@ private async Task FetchCoverImage(CancellationToken cancellationToken) { if (_beatsaver == null) return null!; - var beatmap = await _beatsaver.BeatmapByHash(levelHash, cancellationToken); + var beatmap = await _beatsaver.BeatmapByHash(LevelHash, cancellationToken); if (beatmap == null) return null!; byte[]? coverBytes = await beatmap.LatestVersion.DownloadCoverImage(cancellationToken); diff --git a/MultiplayerCore/Beatmaps/NoInfoBeatmapLevel.cs b/MultiplayerCore/Beatmaps/NoInfoBeatmapLevel.cs index 1a3a3c3..a4184bb 100644 --- a/MultiplayerCore/Beatmaps/NoInfoBeatmapLevel.cs +++ b/MultiplayerCore/Beatmaps/NoInfoBeatmapLevel.cs @@ -2,17 +2,20 @@ namespace MultiplayerCore.Beatmaps { - public class NoInfoBeatmapLevel : MpBeatmapLevel + /// + /// Beatmap level data placeholder, used when no information is available. + /// + public class NoInfoBeatmapLevel : MpBeatmap { - public override string levelHash { get; protected set; } - public override string songName => string.Empty; - public override string songSubName => string.Empty; - public override string songAuthorName => string.Empty; - public override string levelAuthorName => string.Empty; + public override string LevelHash { get; protected set; } + public override string SongName => string.Empty; + public override string SongSubName => string.Empty; + public override string SongAuthorName => string.Empty; + public override string LevelAuthorName => string.Empty; public NoInfoBeatmapLevel(string hash) { - levelHash = hash; + LevelHash = hash; } } } diff --git a/MultiplayerCore/Beatmaps/Packets/MpBeatmapPacket.cs b/MultiplayerCore/Beatmaps/Packets/MpBeatmapPacket.cs index 90c9195..8745638 100644 --- a/MultiplayerCore/Beatmaps/Packets/MpBeatmapPacket.cs +++ b/MultiplayerCore/Beatmaps/Packets/MpBeatmapPacket.cs @@ -1,7 +1,8 @@ -using LiteNetLib.Utils; +using System.Collections.Generic; +using LiteNetLib.Utils; using MultiplayerCore.Beatmaps.Abstractions; +using MultiplayerCore.Beatmaps.Serializable; using MultiplayerCore.Networking.Abstractions; -using System.Collections.Generic; using static SongCore.Data.ExtraSongData; namespace MultiplayerCore.Beatmaps.Packets @@ -16,7 +17,7 @@ public class MpBeatmapPacket : MpPacket public float beatsPerMinute; public float songDuration; - public string characteristic = null!; + public string characteristicName = null!; public BeatmapDifficulty difficulty; public Dictionary requirements = new(); @@ -25,27 +26,20 @@ public class MpBeatmapPacket : MpPacket public MpBeatmapPacket() { } - public MpBeatmapPacket(PreviewDifficultyBeatmap beatmap) + public MpBeatmapPacket(MpBeatmap beatmap, BeatmapKey beatmapKey) { - levelHash = Utilities.HashForLevelID(beatmap.beatmapLevel.levelID); - songName = beatmap.beatmapLevel.songName; - songSubName = beatmap.beatmapLevel.songSubName; - songAuthorName = beatmap.beatmapLevel.songAuthorName; - levelAuthorName = beatmap.beatmapLevel.levelAuthorName; - beatsPerMinute = beatmap.beatmapLevel.beatsPerMinute; - songDuration = beatmap.beatmapLevel.songDuration; - - characteristic = beatmap.beatmapCharacteristic.serializedName; - difficulty = beatmap.beatmapDifficulty; - - if (beatmap.beatmapLevel is MpBeatmapLevel mpBeatmapLevel) - { - if (mpBeatmapLevel.requirements.ContainsKey(beatmap.beatmapCharacteristic.name)) - requirements = mpBeatmapLevel.requirements[beatmap.beatmapCharacteristic.name]; - if (mpBeatmapLevel.requirements.ContainsKey(beatmap.beatmapCharacteristic.serializedName)) - requirements = mpBeatmapLevel.requirements[beatmap.beatmapCharacteristic.serializedName]; - contributors = mpBeatmapLevel.contributors!; - } + levelHash = Utilities.HashForLevelID(beatmap.LevelID); + songName = beatmap.SongName; + songSubName = beatmap.SongSubName; + songAuthorName = beatmap.SongAuthorName; + levelAuthorName = beatmap.LevelAuthorName; + beatsPerMinute = beatmap.BeatsPerMinute; + songDuration = beatmap.SongDuration; + characteristicName = beatmapKey.beatmapCharacteristic.serializedName; + difficulty = beatmapKey.difficulty; + if (beatmap.Requirements.TryGetValue(characteristicName, out var requirementSet)) + requirements = requirementSet; + contributors = beatmap.Contributors!; } public override void Serialize(NetDataWriter writer) @@ -58,7 +52,7 @@ public override void Serialize(NetDataWriter writer) writer.Put(beatsPerMinute); writer.Put(songDuration); - writer.Put(characteristic); + writer.Put(characteristicName); writer.Put((uint)difficulty); writer.Put((byte)requirements.Count); @@ -101,7 +95,7 @@ public override void Deserialize(NetDataReader reader) beatsPerMinute = reader.GetFloat(); songDuration = reader.GetFloat(); - characteristic = reader.GetString(); + characteristicName = reader.GetString(); difficulty = (BeatmapDifficulty)reader.GetUInt(); try diff --git a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs index 7f61611..5f4bae4 100644 --- a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs +++ b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs @@ -1,9 +1,8 @@ -using BeatSaverSharp; -using BeatSaverSharp.Models; +using System.Threading.Tasks; +using BeatSaverSharp; +using MultiplayerCore.Beatmaps.Abstractions; using MultiplayerCore.Beatmaps.Packets; -using SiraUtil.Logging; using SiraUtil.Zenject; -using System.Threading.Tasks; namespace MultiplayerCore.Beatmaps.Providers { @@ -22,7 +21,7 @@ internal MpBeatmapLevelProvider( /// /// The hash of the level to get /// An with a matching level hash - public async Task GetBeatmap(string levelHash) + public async Task GetBeatmap(string levelHash) => GetBeatmapFromLocalBeatmaps(levelHash) ?? await GetBeatmapFromBeatSaver(levelHash); @@ -31,12 +30,13 @@ internal MpBeatmapLevelProvider( /// /// The hash of the level to get /// An with a matching level hash, or null if none was found. - public IPreviewBeatmapLevel? GetBeatmapFromLocalBeatmaps(string levelHash) + public MpBeatmap? GetBeatmapFromLocalBeatmaps(string levelHash) { - IPreviewBeatmapLevel? preview = SongCore.Loader.GetLevelByHash(levelHash); - if (preview == null) + var localBeatmapLevel = SongCore.Loader.GetLevelByHash(levelHash); + if (localBeatmapLevel == null) return null; - return new LocalBeatmapLevel(levelHash, preview); + + return new LocalBeatmapLevel(levelHash, localBeatmapLevel); } /// @@ -44,11 +44,12 @@ internal MpBeatmapLevelProvider( /// /// The hash of the level to get /// An with a matching level hash, or null if none was found. - public async Task GetBeatmapFromBeatSaver(string levelHash) + public async Task GetBeatmapFromBeatSaver(string levelHash) { - Beatmap? beatmap = await _beatsaver.BeatmapByHash(levelHash); + var beatmap = await _beatsaver.BeatmapByHash(levelHash); if (beatmap == null) return null; + return new BeatSaverBeatmapLevel(levelHash, beatmap); } @@ -57,7 +58,7 @@ internal MpBeatmapLevelProvider( /// /// The packet to get preview data from /// An with a cover from BeatSaver. - public IPreviewBeatmapLevel GetBeatmapFromPacket(MpBeatmapPacket packet) + public MpBeatmap GetBeatmapFromPacket(MpBeatmapPacket packet) => new NetworkBeatmapLevel(packet, _beatsaver); } } diff --git a/MultiplayerCore/Beatmaps/Serializable/DifficultyColors.cs b/MultiplayerCore/Beatmaps/Serializable/DifficultyColors.cs new file mode 100644 index 0000000..c75b3d0 --- /dev/null +++ b/MultiplayerCore/Beatmaps/Serializable/DifficultyColors.cs @@ -0,0 +1,111 @@ +using LiteNetLib.Utils; +using static SongCore.Data.ExtraSongData; + +namespace MultiplayerCore.Beatmaps.Serializable +{ + public class DifficultyColors : INetSerializable + { + public MapColor? ColorLeft; + public MapColor? ColorRight; + public MapColor? EnvColorLeft; + public MapColor? EnvColorRight; + public MapColor? EnvColorLeftBoost; + public MapColor? EnvColorRightBoost; + public MapColor? ObstacleColor; + + public bool AnyAreNotNull => ColorLeft != null || ColorRight != null || EnvColorLeft != null || + EnvColorRight != null || EnvColorLeftBoost != null || EnvColorRightBoost != null || + ObstacleColor != null; + + public DifficultyColors() + { + } + + public DifficultyColors(MapColor? colorLeft, MapColor? colorRight, MapColor? envColorLeft, MapColor? envColorRight, MapColor? envColorLeftBoost, MapColor? envColorRightBoost, MapColor? obstacleColor) + { + ColorLeft = colorLeft; + ColorRight = colorRight; + EnvColorLeft = envColorLeft; + EnvColorRight = envColorRight; + EnvColorLeftBoost = envColorLeftBoost; + EnvColorRightBoost = envColorRightBoost; + } + + public void Serialize(NetDataWriter writer) + { + byte colors = (byte)(ColorLeft != null ? 1 : 0); + colors |= (byte)((ColorRight != null ? 1 : 0) << 1); + colors |= (byte)((EnvColorLeft != null ? 1 : 0) << 2); + colors |= (byte)((EnvColorRight != null ? 1 : 0) << 3); + colors |= (byte)((EnvColorLeftBoost != null ? 1 : 0) << 4); + colors |= (byte)((EnvColorRightBoost != null ? 1 : 0) << 5); + colors |= (byte)((ObstacleColor != null ? 1 : 0) << 6); + writer.Put(colors); + + if (ColorLeft != null) + ((MapColorSerializable)ColorLeft).Serialize(writer); + if (ColorRight != null) + ((MapColorSerializable)ColorRight).Serialize(writer); + if (EnvColorLeft != null) + ((MapColorSerializable)EnvColorLeft).Serialize(writer); + if (EnvColorRight != null) + ((MapColorSerializable)EnvColorRight).Serialize(writer); + if (EnvColorLeftBoost != null) + ((MapColorSerializable)EnvColorLeftBoost).Serialize(writer); + if (EnvColorRightBoost != null) + ((MapColorSerializable)EnvColorRightBoost).Serialize(writer); + if (ObstacleColor != null) + ((MapColorSerializable)ObstacleColor).Serialize(writer); + } + + public void Deserialize(NetDataReader reader) + { + var colors = reader.GetByte(); + if ((colors & 0x1) != 0) + ColorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + if (((colors >> 1) & 0x1) != 0) + ColorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + if (((colors >> 2) & 0x1) != 0) + ColorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + if (((colors >> 3) & 0x1) != 0) + ColorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + if (((colors >> 4) & 0x1) != 0) + ColorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + if (((colors >> 5) & 0x1) != 0) + ColorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + if (((colors >> 6) & 0x1) != 0) + ColorLeft = new MapColor(reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + } + + public class MapColorSerializable : INetSerializable + { + public float r; + public float g; + public float b; + + public MapColorSerializable(float red, float green, float blue) + { + r = red; + g = green; + b = blue; + } + + public void Serialize(NetDataWriter writer) + { + writer.Put(r); + writer.Put(g); + writer.Put(b); + } + + public void Deserialize(NetDataReader reader) + { + r = reader.GetFloat(); + g = reader.GetFloat(); + b = reader.GetFloat(); + } + + public static implicit operator MapColor(MapColorSerializable c) => new MapColor(c.r, c.g, c.b); + public static explicit operator MapColorSerializable(MapColor c) => new MapColorSerializable(c.r, c.g, c.b); + } + } +} diff --git a/MultiplayerCore/MultiplayerCore.csproj b/MultiplayerCore/MultiplayerCore.csproj index ae0f3cb..ef43d2f 100644 --- a/MultiplayerCore/MultiplayerCore.csproj +++ b/MultiplayerCore/MultiplayerCore.csproj @@ -47,21 +47,22 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll False False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.Polyglot.dll $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.UnityExtension.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\BGNet.dll - False - False - $(BeatSaberDir)\Beat Saber_Data\Managed\Ignorance.dll False False + + $(BeatSaberDir)\Beat Saber_Data\Managed\BGNetCore.dll + $(BeatSaberDir)\Beat Saber_Data\Managed\BGNetLogging.dll False @@ -76,6 +77,9 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\Colors.dll False + + $(BeatSaberDir)\Beat Saber_Data\Managed\DataModels.dll + $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll False @@ -98,10 +102,6 @@ False False - - $(BeatSaberDir)\Beat Saber_Data\Managed\Polyglot.dll - False - $(BeatSaberDir)\Libs\SemVer.dll False diff --git a/MultiplayerCore/Objects/MpEntitlementChecker.cs b/MultiplayerCore/Objects/MpEntitlementChecker.cs index 6ba8e3a..7a120ff 100644 --- a/MultiplayerCore/Objects/MpEntitlementChecker.cs +++ b/MultiplayerCore/Objects/MpEntitlementChecker.cs @@ -1,13 +1,11 @@ -using BeatSaverSharp; -using BeatSaverSharp.Models; -using SiraUtil.Logging; -using SiraUtil.Zenject; -using SongCore.Data; using System; using System.Collections.Concurrent; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BeatSaverSharp; +using BeatSaverSharp.Models; +using SongCore.Data; using Zenject; using IPALogger = IPA.Logging.Logger; @@ -162,7 +160,7 @@ public Task GetUserEntitlementStatus(string userId, string l /// Remote user /// Level to check entitlement /// Level entitlement status - public EntitlementsStatus GetUserEntitlementStatusWithoutRequest(string userId, string levelId) + public EntitlementsStatus GetKnownEntitlement(string userId, string levelId) { if (_entitlementsDictionary.TryGetValue(userId, out ConcurrentDictionary userDictionary)) if (userDictionary.TryGetValue(levelId, out EntitlementsStatus entitlement)) diff --git a/MultiplayerCore/Objects/MpLevelDownloader.cs b/MultiplayerCore/Objects/MpLevelDownloader.cs index 382156e..d01a476 100644 --- a/MultiplayerCore/Objects/MpLevelDownloader.cs +++ b/MultiplayerCore/Objects/MpLevelDownloader.cs @@ -1,14 +1,14 @@ -using BeatSaverSharp; -using BeatSaverSharp.Models; -using MultiplayerCore.Helpers; -using SiraUtil.Logging; -using SiraUtil.Zenject; -using System; +using System; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BeatSaverSharp; +using BeatSaverSharp.Models; +using MultiplayerCore.Helpers; +using SiraUtil.Logging; +using SiraUtil.Zenject; using UnityEngine; namespace MultiplayerCore.Objects @@ -115,7 +115,7 @@ private async Task DownloadLevel(string levelHash, CancellationToken cancellatio throw result.Exception; } - using (var awaiter = new EventAwaiter>(cancellationToken)) + using (var awaiter = new EventAwaiter>(cancellationToken)) { try { diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index e9221cb..4c6711e 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -1,9 +1,9 @@ -using SiraUtil.Affinity; -using SiraUtil.Logging; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; +using SiraUtil.Logging; namespace MultiplayerCore.Objects { @@ -33,68 +33,135 @@ internal MpLevelLoader( _logger = logger; } + [UsedImplicitly] public new void LoadLevel(ILevelGameplaySetupData gameplaySetupData, long initialStartTime) { - string levelHash = Utilities.HashForLevelID(gameplaySetupData.beatmapLevel.beatmapLevel.levelID); - _logger.Debug($"Loading level {gameplaySetupData.beatmapLevel.beatmapLevel.levelID}"); + var levelId = gameplaySetupData.beatmapKey.levelId; + var levelHash = Utilities.HashForLevelID(levelId); + base.LoadLevel(gameplaySetupData, initialStartTime); - if (levelHash != null && !SongCore.Collections.songWithHashPresent(levelHash)) - _getBeatmapLevelResultTask = DownloadBeatmapLevelAsync(gameplaySetupData.beatmapLevel.beatmapLevel.levelID, _getBeatmapCancellationTokenSource.Token); + + if (levelHash == null) + { + _logger.Debug($"Ignoring level (not a custom level hash): {levelId}"); + return; + } + + var downloadNeeded = !SongCore.Collections.songWithHashPresent(levelHash); + + _logger.Debug($"Loading level: {levelId} (downloadNeeded={downloadNeeded})"); + + if (downloadNeeded) + _getBeatmapLevelResultTask = DownloadBeatmapLevelAsync(levelId, _getBeatmapCancellationTokenSource.Token); } + [UsedImplicitly] public new void Tick() { - if (_loaderState == MultiplayerBeatmapLoaderState.LoadingBeatmap) + if (_loaderState == MultiplayerBeatmapLoaderState.NotLoading) { - base.Tick(); - if (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) - { - _rpcManager.SetIsEntitledToLevel(_gameplaySetupData.beatmapLevel.beatmapLevel.levelID, EntitlementsStatus.Ok); - _logger.Debug($"Loaded level {_gameplaySetupData.beatmapLevel.beatmapLevel.levelID}"); - var hash = Utilities.HashForLevelID(_gameplaySetupData.beatmapLevel.beatmapLevel.levelID); - if (hash != null) - { - var extraSongData = SongCore.Collections.RetrieveExtraSongData(hash); - if (extraSongData != null) - { - var difficulty = extraSongData._difficulties.FirstOrDefault(x => x._difficulty == _gameplaySetupData.beatmapLevel.beatmapDifficulty && x._beatmapCharacteristicName == _gameplaySetupData.beatmapLevel.beatmapCharacteristic.name); - if (difficulty != null && !difficulty.additionalDifficultyData._requirements.All(x => SongCore.Collections.capabilities.Contains(x))) - { - _difficultyBeatmap = null!; - } - } - } - } + // Loader: not doing anything + return; } - else if (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) + + var levelId = _gameplaySetupData.beatmapKey.levelId; + + if (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) { - if (_sessionManager.syncTime >= _startTime) - { - if (_sessionManager.connectedPlayers.All(p => _entitlementChecker.GetUserEntitlementStatusWithoutRequest(p.userId, _gameplaySetupData.beatmapLevel.beatmapLevel.levelID) == EntitlementsStatus.Ok || p.HasState("in_gameplay"))) - { - _logger.Debug($"All players finished loading"); - base.Tick(); - } - } + // Loader: level is loaded locally, waiting for countdown to transition to level + // Modded behavior: wait until all players are ready before we transition + + if (_startTime <= _sessionManager.syncTime) + return; + + // Ready check: player returned OK entitlement (load finished) OR already transitioned to gameplay + var allPlayersReady = _sessionManager.connectedPlayers.All(p => + _entitlementChecker.GetKnownEntitlement(p.userId, levelId) == EntitlementsStatus.Ok || + p.HasState("in_gameplay")); + + if (!allPlayersReady) + return; + + _logger.Debug($"All players finished loading"); + base.Tick(); // calling Tick() now will cause base level loader to transition to gameplay + return; } - else - base.Tick(); + + // Loader main: pending load + base.Tick(); + + var loadDidFinish = (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); + if (!loadDidFinish) + return; + + _rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.Ok); + _logger.Debug($"Loaded level: {levelId}"); + + UnloadLevelIfRequirementsNotMet(); + } + + private void UnloadLevelIfRequirementsNotMet() + { + // Extra: load finished, check if there are extra requirements in place + // If we fail requirements, unload the level + + var beatmapKey = _gameplaySetupData.beatmapKey; + var levelId = beatmapKey.levelId; + + var levelHash = Utilities.HashForLevelID(levelId); + if (levelHash == null) + return; + + var extraSongData = SongCore.Collections.RetrieveExtraSongData(levelHash); + if (extraSongData == null) + return; + + var difficulty = _gameplaySetupData.beatmapKey.difficulty; + var characteristicName = _gameplaySetupData.beatmapKey.beatmapCharacteristic.serializedName; + + var difficultyData = extraSongData._difficulties.FirstOrDefault(x => + x._difficulty == difficulty && x._beatmapCharacteristicName == characteristicName); + if (difficultyData == null) + return; + + var requirementsMet = true; + foreach (var requirement in difficultyData.additionalDifficultyData._requirements) + { + if (SongCore.Collections.capabilities.Contains(requirement)) + continue; + _logger.Warn($"Level requirements not met: {requirement}"); + requirementsMet = false; + } + + if (requirementsMet) + return; + + _logger.Warn($"Level will be unloaded due to unmet requirements"); + _beatmapLevelData = null!; } public void Report(double value) => progressUpdated?.Invoke(value); /// - /// Downloads a level and then loads it. + /// Downloads a custom level, and then loads and returns its data. /// - /// Level to download - /// - /// Level load results - public async Task DownloadBeatmapLevelAsync(string levelId, CancellationToken cancellationToken) + private async Task DownloadBeatmapLevelAsync(string levelId, CancellationToken cancellationToken) { - _ = await _levelDownloader.TryDownloadLevel(levelId, cancellationToken, this); // Handle? - _gameplaySetupData.beatmapLevel.beatmapLevel = _beatmapLevelsModel.GetLevelPreviewForLevelId(levelId); - return await _beatmapLevelsModel.GetBeatmapLevelAsync(levelId, cancellationToken); + // Download from BeatSaver + var success = await _levelDownloader.TryDownloadLevel(levelId, cancellationToken, this); + if (!success) + throw new Exception($"Failed to download level: {levelId}"); + + // Reload custom level set + _logger.Debug("Reloading custom level collection..."); + await _beatmapLevelsModel.ReloadCustomLevelPackCollectionAsync(cancellationToken); + + // Load level data + var loadResult = await _beatmapLevelsModel.LoadBeatmapLevelDataAsync(levelId, cancellationToken); + if (loadResult.isError) + _logger.Error($"Custom level data could not be loaded after download: {levelId}"); + return loadResult; } } } diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index 233057d..889511c 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -1,15 +1,14 @@ -using MultiplayerCore.Beatmaps; -using MultiplayerCore.Beatmaps.Abstractions; +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; using MultiplayerCore.Beatmaps.Packets; using MultiplayerCore.Beatmaps.Providers; using MultiplayerCore.Networking; -using SiraUtil.Affinity; using SiraUtil.Logging; -using System; -using System.Linq; namespace MultiplayerCore.Objects { + [UsedImplicitly] internal class MpPlayersDataModel : LobbyPlayersDataModel, ILobbyPlayersDataModel, IDisposable { private readonly MpPacketSerializer _packetSerializer; @@ -28,7 +27,7 @@ internal MpPlayersDataModel( public new void Activate() { - _packetSerializer.RegisterCallback(HandleMpexBeatmapPacket); + _packetSerializer.RegisterCallback(HandleMpCoreBeatmapPacket); base.Activate(); _menuRpcManager.getRecommendedBeatmapEvent -= base.HandleMenuRpcManagerGetRecommendedBeatmap; _menuRpcManager.getRecommendedBeatmapEvent += this.HandleMenuRpcManagerGetRecommendedBeatmap; @@ -49,47 +48,64 @@ internal MpPlayersDataModel( public new void Dispose() => Deactivate(); - private void HandleMpexBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer player) + private new void SetPlayerBeatmapLevel(string userId, in BeatmapKey beatmapKey) { + // Game: A player (can be the local player!) has selected / recommended a beatmap + + if (userId == _multiplayerSessionManager.localPlayer.userId) + // If local player: send extended beatmap info to other players + _ = SendMpBeatmapPacket(beatmapKey); + + base.SetPlayerBeatmapLevel(userId, in beatmapKey); + } + + private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer player) + { + // Packet: Another player has recommended a beatmap (MpCore), we have received details for the level preview + _logger.Debug($"'{player.userId}' selected song '{packet.levelHash}'."); - BeatmapCharacteristicSO characteristic = _beatmapCharacteristicCollection.GetBeatmapCharacteristicBySerializedName(packet.characteristic); - IPreviewBeatmapLevel preview = _beatmapLevelProvider.GetBeatmapFromPacket(packet); - base.SetPlayerBeatmapLevel(player.userId, new PreviewDifficultyBeatmap(preview, characteristic, packet.difficulty)); + + var beatmap = _beatmapLevelProvider.GetBeatmapFromPacket(packet); + var characteristic = _beatmapCharacteristicCollection.GetBeatmapCharacteristicBySerializedName(packet.characteristicName); + + // TODO: How can we present our custom data in the base game UI? + base.SetPlayerBeatmapLevel(player.userId, new BeatmapKey(beatmap.LevelID, characteristic, packet.difficulty)); } - public new void HandleMenuRpcManagerGetRecommendedBeatmap(string userId) + private new void HandleMenuRpcManagerGetRecommendedBeatmap(string userId) { - ILobbyPlayerData localPlayerData = _playersData[localUserId]; - string? levelId = localPlayerData.beatmapLevel?.beatmapLevel?.levelID; - if (string.IsNullOrEmpty(levelId)) - return; - string? levelHash = Utilities.HashForLevelID(levelId!); - if (!string.IsNullOrEmpty(levelHash)) - _multiplayerSessionManager.Send(new MpBeatmapPacket(localPlayerData.beatmapLevel!)); + // RPC: The server / another player has asked us to send our recommended beatmap + + var selectedBeatmapKey = _playersData[localUserId].beatmapKey; + _ = SendMpBeatmapPacket(selectedBeatmapKey); base.HandleMenuRpcManagerGetRecommendedBeatmap(userId); } - public new void HandleMenuRpcManagerRecommendBeatmap(string userId, BeatmapIdentifierNetSerializable beatmapId) + private new void HandleMenuRpcManagerRecommendBeatmap(string userId, BeatmapKeyNetSerializable beatmapKeySerializable) { - if (!string.IsNullOrEmpty(Utilities.HashForLevelID(beatmapId.levelID))) + // RPC: Another player has recommended a beatmap (base game) + + if (!string.IsNullOrEmpty(Utilities.HashForLevelID(beatmapKeySerializable.levelID))) return; - base.HandleMenuRpcManagerRecommendBeatmap(userId, beatmapId); + + base.HandleMenuRpcManagerRecommendBeatmap(userId, beatmapKeySerializable); } - public new async void SetLocalPlayerBeatmapLevel(PreviewDifficultyBeatmap beatmapLevel) + private async Task SendMpBeatmapPacket(BeatmapKey beatmapKey) { - _logger.Debug($"Local player selected song '{beatmapLevel.beatmapLevel.levelID}'"); - string? levelHash = Utilities.HashForLevelID(beatmapLevel.beatmapLevel.levelID); - if (!string.IsNullOrEmpty(levelHash)) - { - if (beatmapLevel.beatmapLevel is not MpBeatmapLevel) - beatmapLevel.beatmapLevel = await _beatmapLevelProvider.GetBeatmap(levelHash); - _multiplayerSessionManager.Send(new MpBeatmapPacket(beatmapLevel)); - base.SetLocalPlayerBeatmapLevel(beatmapLevel); + var levelId = beatmapKey.levelId; + + var levelHash = Utilities.HashForLevelID(levelId); + if (levelHash == null) + return; + + var levelData = await _beatmapLevelProvider.GetBeatmap(levelHash); + if (levelData == null) return; - } - base.SetLocalPlayerBeatmapLevel(beatmapLevel); + + var packet = new MpBeatmapPacket(levelData, beatmapKey); + _multiplayerSessionManager.Send(packet); } } } diff --git a/MultiplayerCore/Patchers/CustomLevelsPatcher.cs b/MultiplayerCore/Patchers/CustomLevelsPatcher.cs index c45ed28..d777d67 100644 --- a/MultiplayerCore/Patchers/CustomLevelsPatcher.cs +++ b/MultiplayerCore/Patchers/CustomLevelsPatcher.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using HarmonyLib; -using MultiplayerCore.Beatmaps; using SiraUtil.Affinity; using SiraUtil.Logging; using UnityEngine.UI; @@ -40,14 +39,6 @@ private static void SetPlayersMissingLevelText(LobbySetupViewController __instan __instance.SetStartGameEnabled(CannotStartGameReason.DoNotOwnSong); } - [HarmonyPostfix] - [HarmonyPatch(typeof(BeatmapIdentifierNetSerializableHelper), nameof(BeatmapIdentifierNetSerializableHelper.ToPreviewDifficultyBeatmap))] - private static void BeatmapIdentifierToPreviewDifficultyBeatmap(BeatmapIdentifierNetSerializable beatmapId, ref PreviewDifficultyBeatmap __result) - { - if (__result.beatmapLevel == null) - __result.beatmapLevel = new NoInfoBeatmapLevel(Utilities.HashForLevelID(beatmapId.levelID)); - } - [AffinityPrefix] [AffinityPatch(typeof(JoinQuickPlayViewController), nameof(JoinQuickPlayViewController.Setup))] private void SetupPre(JoinQuickPlayViewController __instance, ref BeatmapDifficultyDropdown ____beatmapDifficultyDropdown) @@ -68,13 +59,5 @@ private void IsQuickPlaySetupTaskValid(QuickPlaySetupModel __instance, ref bool { if (_networkConfig.IsOverridingApi) __result = false; } - - //[AffinityPostfix] - //[AffinityPatch(typeof(MultiplayerModeSelectionFlowCoordinator), nameof(MultiplayerModeSelectionFlowCoordinator.HandleJoinQuickPlayViewControllerDidFinish))] - //private void HandleJoinQuickPlayViewControllerDidFinish(MultiplayerModeSelectionFlowCoordinator __instance, ref bool __result, Task ____request, DateTime ____lastRequestTime) - //{ - // // TODO: Possibly add warning screen. - //} - } } diff --git a/MultiplayerCore/Patchers/UpdateMapPatcher.cs b/MultiplayerCore/Patchers/UpdateMapPatcher.cs index 5916e68..574410f 100644 --- a/MultiplayerCore/Patchers/UpdateMapPatcher.cs +++ b/MultiplayerCore/Patchers/UpdateMapPatcher.cs @@ -1,87 +1,84 @@ -using BeatSaverSharp; -using HarmonyLib; -using HMUI; -using IPA.Utilities; -using IPA.Utilities.Async; -using MultiplayerCore.Beatmaps.Abstractions; -using Polyglot; -using SiraUtil.Affinity; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace MultiplayerCore.Patchers -{ - [HarmonyPatch] - internal class UpdateMapPatcher : IAffinity - { - public const string PlayersMissingLevelTextKey = "LABEL_PLAYERS_MISSING_ENTITLEMENT"; - - private CancellationTokenSource? _beatmapCts; - - private LobbySetupViewController _lobbySetupViewController; - private readonly ILobbyPlayersDataModel _playersDataModel; - private readonly BeatSaver _beatsaver; - - public UpdateMapPatcher( - LobbySetupViewController lobbySetupViewController, - ILobbyPlayersDataModel playersDataModel, - BeatSaver beatsaver) - { - _lobbySetupViewController = lobbySetupViewController; - _playersDataModel = playersDataModel; - _beatsaver = beatsaver; - } - - [AffinityPrefix] - [AffinityPatch(typeof(GameServerLobbyFlowCoordinator), nameof(GameServerLobbyFlowCoordinator.HandleMenuRpcManagerSetPlayersMissingEntitlementsToLevel))] - private bool SetPlayersMissingEntitlementsToLevel(PlayersMissingEntitlementsNetSerializable playersMissingEntitlements) - { - var levelId = _playersDataModel[_playersDataModel.localUserId].beatmapLevel?.beatmapLevel.levelID; - var levelHash = Utilities.HashForLevelID(levelId); - if (levelId is null || levelHash is null) - return true; - _beatmapCts?.Cancel(); - _beatmapCts = new CancellationTokenSource(); - _ = Task.Run(() => FetchAndShowError(levelHash, playersMissingEntitlements, _beatmapCts.Token), _beatmapCts.Token); - return false; - } - - private async Task FetchAndShowError(string levelHash, PlayersMissingEntitlementsNetSerializable playersMissingEntitlements, CancellationToken cancellationToken) - { - var beatmap = await _beatsaver.BeatmapByHash(levelHash, cancellationToken); - - string errorText = PlayersMissingLevelTextKey; - if (beatmap?.LatestVersion.Hash != levelHash) - errorText = "Click here to update this song. These players cannot download the older version"; - if (_playersDataModel[_playersDataModel.localUserId].beatmapLevel?.beatmapLevel is MpBeatmapLevel beatmapLevel && beatmapLevel.requirements.Any()) - errorText = "This map has mod requirements that these players may not have"; - - await UnityMainThreadTaskScheduler.Factory.StartNew(() => SetPlayersMissingLevelText(playersMissingEntitlements, errorText)); - } - - private static readonly FieldInfo _errorField = AccessTools.Field(typeof(UpdateMapPatcher), nameof(_errorText)); - - [HarmonyReversePatch] - [HarmonyPatch(typeof(GameServerLobbyFlowCoordinator), nameof(GameServerLobbyFlowCoordinator.HandleMenuRpcManagerSetPlayersMissingEntitlementsToLevel))] - private static void SetPlayersMissingLevelText(PlayersMissingEntitlementsNetSerializable playersMissingEntitlements, string errorText) - { - _errorText = errorText; - IEnumerable Transpiler(IEnumerable instructions) => - new CodeMatcher(instructions) - .Start() - .RemoveInstructions(2) - .Set(OpCodes.Ldc_I4_1, null) - .MatchForward(false, new CodeMatch(i => i.opcode == OpCodes.Ldstr && i.OperandIs(PlayersMissingLevelTextKey))) - .Set(OpCodes.Ldsfld, _errorField) - .InstructionEnumeration(); - _ = Transpiler(null!); - } - - private static string? _errorText = null; - } -} \ No newline at end of file +// using BeatSaverSharp; +// using HarmonyLib; +// using IPA.Utilities.Async; +// using MultiplayerCore.Beatmaps.Abstractions; +// using SiraUtil.Affinity; +// using System.Collections.Generic; +// using System.Linq; +// using System.Reflection; +// using System.Reflection.Emit; +// using System.Threading; +// using System.Threading.Tasks; +// +// namespace MultiplayerCore.Patchers +// { +// [HarmonyPatch] +// internal class UpdateMapPatcher : IAffinity +// { +// public const string PlayersMissingLevelTextKey = "LABEL_PLAYERS_MISSING_ENTITLEMENT"; +// +// private CancellationTokenSource? _beatmapCts; +// +// private LobbySetupViewController _lobbySetupViewController; +// private readonly ILobbyPlayersDataModel _playersDataModel; +// private readonly BeatSaver _beatsaver; +// +// public UpdateMapPatcher( +// LobbySetupViewController lobbySetupViewController, +// ILobbyPlayersDataModel playersDataModel, +// BeatSaver beatsaver) +// { +// _lobbySetupViewController = lobbySetupViewController; +// _playersDataModel = playersDataModel; +// _beatsaver = beatsaver; +// } +// +// [AffinityPrefix] +// [AffinityPatch(typeof(GameServerLobbyFlowCoordinator), nameof(GameServerLobbyFlowCoordinator.HandleMenuRpcManagerSetPlayersMissingEntitlementsToLevel))] +// private bool SetPlayersMissingEntitlementsToLevel(PlayersMissingEntitlementsNetSerializable playersMissingEntitlements) +// { +// var levelId = _playersDataModel[_playersDataModel.localUserId].beatmapLevel?.beatmapLevel.levelID; +// var levelHash = Utilities.HashForLevelID(levelId); +// if (levelId is null || levelHash is null) +// return true; +// _beatmapCts?.Cancel(); +// _beatmapCts = new CancellationTokenSource(); +// _ = Task.Run(() => FetchAndShowError(levelHash, playersMissingEntitlements, _beatmapCts.Token), _beatmapCts.Token); +// return false; +// } +// +// private async Task FetchAndShowError(string levelHash, PlayersMissingEntitlementsNetSerializable playersMissingEntitlements, CancellationToken cancellationToken) +// { +// var beatmap = await _beatsaver.BeatmapByHash(levelHash, cancellationToken); +// +// string errorText = PlayersMissingLevelTextKey; +// if (beatmap?.LatestVersion.Hash != levelHash) +// errorText = "Click here to update this song. These players cannot download the older version"; +// if (_playersDataModel[_playersDataModel.localUserId].beatmapLevel?.beatmapLevel is MpBeatmap beatmapLevel && beatmapLevel.Requirements.Any()) +// errorText = "This map has mod requirements that these players may not have"; +// +// await UnityMainThreadTaskScheduler.Factory.StartNew(() => SetPlayersMissingLevelText(playersMissingEntitlements, errorText)); +// } +// +// private static readonly FieldInfo _errorField = AccessTools.Field(typeof(UpdateMapPatcher), nameof(_errorText)); +// +// [HarmonyReversePatch] +// [HarmonyPatch(typeof(GameServerLobbyFlowCoordinator), nameof(GameServerLobbyFlowCoordinator.HandleMenuRpcManagerSetPlayersMissingEntitlementsToLevel))] +// private static void SetPlayersMissingLevelText(PlayersMissingEntitlementsNetSerializable playersMissingEntitlements, string errorText) +// { +// _errorText = errorText; +// IEnumerable Transpiler(IEnumerable instructions) => +// new CodeMatcher(instructions) +// .Start() +// .RemoveInstructions(2) +// .Set(OpCodes.Ldc_I4_1, null) +// .MatchForward(false, new CodeMatch(i => i.opcode == OpCodes.Ldstr && i.OperandIs(PlayersMissingLevelTextKey))) +// .Set(OpCodes.Ldsfld, _errorField) +// .InstructionEnumeration(); +// _ = Transpiler(null!); +// } +// +// private static string? _errorText = null; +// } +// } +// TODO Review / test / rework as needed \ No newline at end of file diff --git a/MultiplayerCore/Plugin.cs b/MultiplayerCore/Plugin.cs index 584d293..0aab2b6 100644 --- a/MultiplayerCore/Plugin.cs +++ b/MultiplayerCore/Plugin.cs @@ -1,12 +1,11 @@ -using BeatSaverSharp; +using System; +using System.IO; +using BeatSaverSharp; using HarmonyLib; using IPA; -using IPA.Config; using IPA.Loader; using MultiplayerCore.Installers; using SiraUtil.Zenject; -using System; -using System.IO; using UnityEngine; using IPALogger = IPA.Logging.Logger; @@ -43,7 +42,7 @@ public Plugin(IPALogger logger, PluginMetadata pluginMetadata, Zenjector zenject [OnEnable] public void OnEnable() { - SongCore.Collections.AddSeperateSongFolder("Multiplayer", Path.Combine(Application.dataPath, CustomLevelsPath), SongCore.Data.FolderLevelPack.CustomLevels); + SongCore.Collections.AddSeparateSongFolder("Multiplayer", Path.Combine(Application.dataPath, CustomLevelsPath), SongCore.Data.FolderLevelPack.CustomLevels); _harmony.PatchAll(_metadata.Assembly); } diff --git a/MultiplayerCore/UI/MpColorsUI.cs b/MultiplayerCore/UI/MpColorsUI.cs index 9703c3f..1a93695 100644 --- a/MultiplayerCore/UI/MpColorsUI.cs +++ b/MultiplayerCore/UI/MpColorsUI.cs @@ -6,7 +6,7 @@ using BeatSaberMarkupLanguage.Components; using BeatSaberMarkupLanguage.Components.Settings; using HMUI; -using MultiplayerCore.Beatmaps.Abstractions; +using MultiplayerCore.Beatmaps.Serializable; using MultiplayerCore.Helpers; using UnityEngine; @@ -101,13 +101,13 @@ private void Dismiss() private void SetColors(DifficultyColors colors) { - Color saberLeft = colors._colorLeft == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors._colorLeft); - Color saberRight = colors._colorRight == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors._colorRight); - Color envLeft = colors._envColorLeft == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors._envColorLeft); - Color envRight = colors._envColorRight == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors._envColorRight); - Color envLeftBoost = colors._envColorLeftBoost == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors._envColorLeftBoost); - Color envRightBoost = colors._envColorRightBoost == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors._envColorRightBoost); - Color obstacle = colors._obstacleColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors._obstacleColor); + Color saberLeft = colors.ColorLeft == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.ColorLeft); + Color saberRight = colors.ColorRight == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.ColorRight); + Color envLeft = colors.EnvColorLeft == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.EnvColorLeft); + Color envRight = colors.EnvColorRight == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.EnvColorRight); + Color envLeftBoost = colors.EnvColorLeftBoost == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.EnvColorLeftBoost); + Color envRightBoost = colors.EnvColorRightBoost == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.EnvColorRightBoost); + Color obstacle = colors.ObstacleColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.ObstacleColor); colorSchemeView.SetColors(saberLeft, saberRight, envLeft, envRight, envLeftBoost, envRightBoost, obstacle); } diff --git a/MultiplayerCore/UI/MpLoadingIndicator.cs b/MultiplayerCore/UI/MpLoadingIndicator.cs index ca771ff..3a7b9d4 100644 --- a/MultiplayerCore/UI/MpLoadingIndicator.cs +++ b/MultiplayerCore/UI/MpLoadingIndicator.cs @@ -1,10 +1,10 @@ -using BeatSaberMarkupLanguage; +using System; +using System.Linq; +using System.Reflection; +using BeatSaberMarkupLanguage; using BeatSaberMarkupLanguage.Attributes; using BeatSaberMarkupLanguage.Components; using MultiplayerCore.Objects; -using System; -using System.Linq; -using System.Reflection; using UnityEngine; using Zenject; @@ -60,8 +60,9 @@ public void Tick() { if (_isDownloading) return; - else if (_screenController.countdownShown && _sessionManager.syncTime >= _gameStateController.startTime && _gameStateController.levelStartInitiated && _levelLoader.CurrentLoadingData != null) - _loadingControl.ShowLoading($"{_playersDataModel.Count(x => _entitlementChecker.GetUserEntitlementStatusWithoutRequest(x.Key, _levelLoader.CurrentLoadingData.beatmapLevel.beatmapLevel.levelID) == EntitlementsStatus.Ok) + 1} of {_playersDataModel.Count - 1} players ready..."); + + if (_screenController.countdownShown && _sessionManager.syncTime >= _gameStateController.startTime && _gameStateController.levelStartInitiated && _levelLoader.CurrentLoadingData != null) + _loadingControl.ShowLoading($"{_playersDataModel.Count(x => _entitlementChecker.GetKnownEntitlement(x.Key, _levelLoader.CurrentLoadingData.beatmapKey.levelId) == EntitlementsStatus.Ok) + 1} of {_playersDataModel.Count - 1} players ready..."); else _loadingControl.Hide(); } diff --git a/MultiplayerCore/UI/MpRequirementsUI.cs b/MultiplayerCore/UI/MpRequirementsUI.cs index 2990354..66204c8 100644 --- a/MultiplayerCore/UI/MpRequirementsUI.cs +++ b/MultiplayerCore/UI/MpRequirementsUI.cs @@ -1,19 +1,13 @@ using System; -using System.IO; -using System.Linq; using System.Reflection; using BeatSaberMarkupLanguage; using BeatSaberMarkupLanguage.Attributes; using BeatSaberMarkupLanguage.Components; using HMUI; using IPA.Utilities; -using MultiplayerCore.Beatmaps; -using MultiplayerCore.Beatmaps.Abstractions; -using MultiplayerCore.Helpers; using SiraUtil.Logging; using UnityEngine; using Zenject; -using static BeatSaberMarkupLanguage.Components.CustomListTableData; namespace MultiplayerCore.UI { @@ -111,20 +105,21 @@ public void Dispose() private void BeatmapSelected(string a) { - var beatmapLevel = _playersDataModel[_playersDataModel.localUserId].beatmapLevel; - if (beatmapLevel?.beatmapLevel is MpBeatmapLevel mpLevel) - { - string characteristicName = null!; - if (mpLevel.difficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.name)) - characteristicName = beatmapLevel.beatmapCharacteristic.name; - else if (mpLevel.difficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName)) - characteristicName = beatmapLevel.beatmapCharacteristic.serializedName; - if (characteristicName != null && mpLevel.difficultyColors[characteristicName].TryGetValue(beatmapLevel.beatmapDifficulty, out var colors)) - ButtonInteractable = colors.AnyAreNotNull; - else - ButtonInteractable = (characteristicName != null && mpLevel.requirements[characteristicName].Any()) || (mpLevel.contributors?.Any() ?? false); - } - else + var beatmapLevel = _playersDataModel[_playersDataModel.localUserId].beatmapKey; + // TODO: Load MpBeatmap data from somewhere + // if (beatmapLevel?.beatmapLevel is MpBeatmap mpLevel) + // { + // string characteristicName = null!; + // if (mpLevel.DifficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.name)) + // characteristicName = beatmapLevel.beatmapCharacteristic.name; + // else if (mpLevel.DifficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName)) + // characteristicName = beatmapLevel.beatmapCharacteristic.serializedName; + // if (characteristicName != null && mpLevel.DifficultyColors[characteristicName].TryGetValue(beatmapLevel.beatmapDifficulty, out var colors)) + // ButtonInteractable = colors.AnyAreNotNull; + // else + // ButtonInteractable = (characteristicName != null && mpLevel.Requirements[characteristicName].Any()) || (mpLevel.Contributors?.Any() ?? false); + // } + // else ButtonInteractable = false; } @@ -148,73 +143,75 @@ internal void ShowRequirements() if (!_playersDataModel.ContainsKey(localUserId)) return; var localPlayerDataModel = _playersDataModel[localUserId]; - var level = localPlayerDataModel.beatmapLevel; - - if (level.beatmapLevel is MpBeatmapLevel mpLevel) - { - string characteristicName = null!; - if (mpLevel.requirements.ContainsKey(level.beatmapCharacteristic.name) || mpLevel.difficultyColors.ContainsKey(level.beatmapCharacteristic.name)) - characteristicName = level.beatmapCharacteristic.name; - else if (mpLevel.requirements.ContainsKey(level.beatmapCharacteristic.serializedName) || mpLevel.difficultyColors.ContainsKey(level.beatmapCharacteristic.serializedName)) - characteristicName = level.beatmapCharacteristic.serializedName; - - // Requirements - if (mpLevel.requirements.TryGetValue(characteristicName, out var difficultiesRequirements)) - if (difficultiesRequirements.TryGetValue(level.beatmapDifficulty, out var difficultyRequirements) && difficultyRequirements.Any()) - foreach (string req in difficultyRequirements) - customListTableData.data.Add(!SongCore.Collections.capabilities.Contains(req) - ? new CustomCellInfo($"{req}", "Missing Requirement", MissingReqIcon) - : new CustomCellInfo($"{req}", "Requirement", HaveReqIcon)); - - // Contributors - if (mpLevel.contributors != null) - foreach (var contributor in mpLevel.contributors) - { - if (contributor.icon == null) - { - if (!string.IsNullOrWhiteSpace(contributor._iconPath) && !string.IsNullOrEmpty(contributor._iconPath) && SongCore.Collections.songWithHashPresent(mpLevel.levelHash)) - { - var songCoreLevel = SongCore.Loader.GetLevelByHash(mpLevel.levelHash); - contributor.icon = SongCore.Utilities.Utils.LoadSpriteFromFile(Path.Combine(songCoreLevel!.customLevelPath, contributor._iconPath)); - customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, contributor.icon != null ? contributor.icon : InfoIcon)); - } - else - customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, InfoIcon)); - } - else - customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, contributor.icon)); - } - - // Colors - var customColorsEnabled = SongCoreConfig.AnyCustomSongColors; - if (mpLevel.difficultyColors.TryGetValue(characteristicName, out var difficultyColors) && difficultyColors.TryGetValue(level.beatmapDifficulty, out var colors) && (colors.AnyAreNotNull)) - customListTableData.data.Add(new CustomCellInfo($"Custom Colors Available", $"Click here to preview & {(customColorsEnabled ? "disable" : "enable")} it.", ColorsIcon)); - else if (mpLevel is BeatSaverBeatmapLevel) - customListTableData.data.Add(new CustomCellInfo($"Custom Colors", $"Click here to preview & {(customColorsEnabled ? "disable" : "enable")} it.", ColorsIcon)); - - customListTableData.tableView.ReloadData(); - customListTableData.tableView.ScrollToCellWithIdx(0, TableView.ScrollPositionType.Beginning, false); - } + var level = localPlayerDataModel.beatmapKey; + + // TODO: Load MpBeatmap data from somewhere + // if (level.beatmapLevel is MpBeatmap mpLevel) + // { + // string characteristicName = null!; + // if (mpLevel.Requirements.ContainsKey(level.beatmapCharacteristic.name) || mpLevel.DifficultyColors.ContainsKey(level.beatmapCharacteristic.name)) + // characteristicName = level.beatmapCharacteristic.name; + // else if (mpLevel.Requirements.ContainsKey(level.beatmapCharacteristic.serializedName) || mpLevel.DifficultyColors.ContainsKey(level.beatmapCharacteristic.serializedName)) + // characteristicName = level.beatmapCharacteristic.serializedName; + // + // // Requirements + // if (mpLevel.Requirements.TryGetValue(characteristicName, out var difficultiesRequirements)) + // if (difficultiesRequirements.TryGetValue(level.beatmapDifficulty, out var difficultyRequirements) && difficultyRequirements.Any()) + // foreach (string req in difficultyRequirements) + // customListTableData.data.Add(!SongCore.Collections.capabilities.Contains(req) + // ? new CustomCellInfo($"{req}", "Missing Requirement", MissingReqIcon) + // : new CustomCellInfo($"{req}", "Requirement", HaveReqIcon)); + // + // // Contributors + // if (mpLevel.Contributors != null) + // foreach (var contributor in mpLevel.Contributors) + // { + // if (contributor.icon == null) + // { + // if (!string.IsNullOrWhiteSpace(contributor._iconPath) && !string.IsNullOrEmpty(contributor._iconPath) && SongCore.Collections.songWithHashPresent(mpLevel.LevelHash)) + // { + // var songCoreLevel = SongCore.Loader.GetLevelByHash(mpLevel.LevelHash); + // contributor.icon = SongCore.Utilities.Utils.LoadSpriteFromFile(Path.Combine(songCoreLevel!.customLevelPath, contributor._iconPath)); + // customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, contributor.icon != null ? contributor.icon : InfoIcon)); + // } + // else + // customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, InfoIcon)); + // } + // else + // customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, contributor.icon)); + // } + // + // // Colors + // var customColorsEnabled = SongCoreConfig.AnyCustomSongColors; + // if (mpLevel.DifficultyColors.TryGetValue(characteristicName, out var difficultyColors) && difficultyColors.TryGetValue(level.beatmapDifficulty, out var colors) && (colors.AnyAreNotNull)) + // customListTableData.data.Add(new CustomCellInfo($"Custom Colors Available", $"Click here to preview & {(customColorsEnabled ? "disable" : "enable")} it.", ColorsIcon)); + // else if (mpLevel is BeatSaverBeatmapLevel) + // customListTableData.data.Add(new CustomCellInfo($"Custom Colors", $"Click here to preview & {(customColorsEnabled ? "disable" : "enable")} it.", ColorsIcon)); + // + // customListTableData.tableView.ReloadData(); + // customListTableData.tableView.ScrollToCellWithIdx(0, TableView.ScrollPositionType.Beginning, false); + // } } [UIAction("list-select")] private void Select(TableView _, int index) { var localUserData = _playersDataModel[_playersDataModel.localUserId]; - var beatmapLevel = localUserData.beatmapLevel; - - if (beatmapLevel.beatmapLevel is MpBeatmapLevel mpLevel) - { - string characteristicName = null!; - if (mpLevel.requirements.ContainsKey(beatmapLevel.beatmapCharacteristic.name) || mpLevel.difficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.name)) - characteristicName = beatmapLevel.beatmapCharacteristic.name; - else if (mpLevel.requirements.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName) || mpLevel.difficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName)) - characteristicName = beatmapLevel.beatmapCharacteristic.serializedName; - - customListTableData.tableView.ClearSelection(); - if (customListTableData.data[index].icon == ColorsIcon) - _modal.Hide(false, () => _colorsUI.ShowColors(mpLevel.difficultyColors[characteristicName][beatmapLevel.beatmapDifficulty])); - } + var beatmapLevel = localUserData.beatmapKey; + + // TODO: Load MpBeatmap data from somewhere + // if (beatmapLevel.beatmapLevel is MpBeatmap mpLevel) + // { + // string characteristicName = null!; + // if (mpLevel.Requirements.ContainsKey(beatmapLevel.beatmapCharacteristic.name) || mpLevel.DifficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.name)) + // characteristicName = beatmapLevel.beatmapCharacteristic.name; + // else if (mpLevel.Requirements.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName) || mpLevel.DifficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName)) + // characteristicName = beatmapLevel.beatmapCharacteristic.serializedName; + // + // customListTableData.tableView.ClearSelection(); + // if (customListTableData.data[index].icon == ColorsIcon) + // _modal.Hide(false, () => _colorsUI.ShowColors(mpLevel.DifficultyColors[characteristicName][beatmapLevel.beatmapDifficulty])); + // } } } } diff --git a/MultiplayerCore/Utilities.cs b/MultiplayerCore/Utilities.cs index 03097e2..44dd9e0 100644 --- a/MultiplayerCore/Utilities.cs +++ b/MultiplayerCore/Utilities.cs @@ -1,8 +1,8 @@ namespace MultiplayerCore { - internal class Utilities + internal static class Utilities { - public static string HashForLevelID(string? levelId) + internal static string? HashForLevelID(string? levelId) { if (string.IsNullOrWhiteSpace(levelId)) return null!; @@ -12,7 +12,7 @@ public static string HashForLevelID(string? levelId) hash = ary[2]; if ((hash?.Length ?? 0) == 40) return hash!; - return null!; + return null; } } } diff --git a/MultiplayerCore/manifest.json b/MultiplayerCore/manifest.json index 1d689f5..dc67d50 100644 --- a/MultiplayerCore/manifest.json +++ b/MultiplayerCore/manifest.json @@ -5,15 +5,15 @@ "author": "Goobwabber", "version": "1.5.0", "description": "Adds custom songs to Beat Saber Multiplayer.", - "gameVersion": "1.31.0", + "gameVersion": "1.35.0", "dependsOn": { - "BSIPA": "^4.3.0", - "SongCore": "^3.10.3", - "BeatSaverSharp": "^3.4.4", - "SiraUtil": "^3.1.3", - "BeatSaberMarkupLanguage": "^1.7.3", - "System.IO.Compression": "^4.6.0", - "System.IO.Compression.FileSystem": "^4.7.0" + "BSIPA": "^4.3.3", + "SongCore": "^3.13.0", + "BeatSaverSharp": "^3.4.5", + "SiraUtil": "^3.1.7", + "BeatSaberMarkupLanguage": "^1.9.0", + "System.IO.Compression": "^4.6.57", + "System.IO.Compression.FileSystem": "^4.7.3056" }, "links": { "project-home": "https://github.com/Goobwabber/MultiplayerCore", From e4a37fc01b87b4a741bcebd35c697164f10a3545 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Fri, 5 Apr 2024 01:08:49 +0200 Subject: [PATCH 02/75] feat: "required" flag for RequiredMod check --- MultiplayerCore/Models/MpStatusData.cs | 15 +++++++++++++-- .../MultiplayerUnavailableReasonPatches.cs | 7 ++++--- .../Repositories/MpStatusRepository.cs | 5 ++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/MultiplayerCore/Models/MpStatusData.cs b/MultiplayerCore/Models/MpStatusData.cs index c9e5e4a..c056ec6 100644 --- a/MultiplayerCore/Models/MpStatusData.cs +++ b/MultiplayerCore/Models/MpStatusData.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json; -using System; +using System; +using Newtonsoft.Json; namespace MultiplayerCore.Models { @@ -59,8 +59,19 @@ private bool _useSsl [Serializable] public class RequiredMod { + /// + /// BSIPA Mod ID. + /// public string id = null!; + /// + /// Minimum version of the mod required. + /// public string version = null!; + /// + /// Indicates whether the mod is required or not. + /// If false, only minimum versions are enforced. + /// + public bool required = false; } } } diff --git a/MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs b/MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs index 10470e4..4033e53 100644 --- a/MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs +++ b/MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs @@ -3,7 +3,6 @@ using IPA.Loader; using IPA.Utilities; using MultiplayerCore.Models; -using System.Windows.Forms; namespace MultiplayerCore.Patches { @@ -26,11 +25,13 @@ private static bool TryGetMultiplayerUnavailableReasonPrefix(MultiplayerStatusDa foreach (var requiredMod in mpData.requiredMods) { var metadata = PluginManager.GetPluginFromId(requiredMod.id); - if (metadata == null) + if (metadata == null && !requiredMod.required) + // Optional mod is not installed continue; var requiredVersion = new Version(requiredMod.version); - if (requiredVersion <= metadata.HVersion) + if (metadata != null && metadata.HVersion >= requiredVersion) + // Mod is installed and up to date continue; reason = (MultiplayerUnavailableReason)5; diff --git a/MultiplayerCore/Repositories/MpStatusRepository.cs b/MultiplayerCore/Repositories/MpStatusRepository.cs index 32ad0b6..708ca83 100644 --- a/MultiplayerCore/Repositories/MpStatusRepository.cs +++ b/MultiplayerCore/Repositories/MpStatusRepository.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using HarmonyLib; using MultiplayerCore.Models; -using MultiplayerCore.Patchers; using SiraUtil.Affinity; using SiraUtil.Logging; @@ -70,7 +68,8 @@ private void RaiseUpdateEvent(string url, MpStatusData statusData) #region Patch [AffinityPrefix] - [AffinityPatch(typeof(MultiplayerUnavailableReasonMethods), "TryGetMultiplayerUnavailableReason")] + [AffinityPatch(typeof(MultiplayerUnavailableReasonMethods), + nameof(MultiplayerUnavailableReasonMethods.TryGetMultiplayerUnavailableReason))] private void PrefixTryGetMultiplayerUnavailableReason(MultiplayerStatusData data) { // TryGetMultiplayerUnavailableReason is called whenever a server response is parsed From 3a7e582eaa71076866d2e0990b4c9363fda0c399 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Fri, 5 Apr 2024 01:49:28 +0200 Subject: [PATCH 03/75] wip(1.35): new extended status fields https://github.com/Goobwabber/MultiplayerCore/wiki/Multiplayer-Status --- MultiplayerCore/Models/MpStatusData.cs | 103 ++++++++++++++----------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/MultiplayerCore/Models/MpStatusData.cs b/MultiplayerCore/Models/MpStatusData.cs index c056ec6..c68e632 100644 --- a/MultiplayerCore/Models/MpStatusData.cs +++ b/MultiplayerCore/Models/MpStatusData.cs @@ -1,60 +1,74 @@ using System; using Newtonsoft.Json; +// ReSharper disable InconsistentNaming namespace MultiplayerCore.Models { [Serializable] public class MpStatusData : MultiplayerStatusData { + /// + /// Handled by MultiplayerCore. If defined, and if a mod with a bad version is found, the multiplayer status + /// check fails and MUR-5 is returned. + /// [JsonProperty("required_mods")] - private RequiredMod[] _requiredMods - { - get - { - return requiredMods; - } - - set - { - requiredMods = value; - } - } + public RequiredMod[]? requiredMods { get; set; } + /// + /// Handled by MultiplayerCore. If defined, and if the current game version exceeds this version, the + /// multiplayer status check fails and MUR-6 is returned. + /// [JsonProperty("maximum_app_version")] - private string _maximumAppVersion - { - get - { - return maximumAppVersion; - } + public string? maximumAppVersion { get; set; } - set - { - maximumAppVersion = value; - } - } - + /// + /// Information only. Indicates whether dedicated server connections should use SSL/TLS. Currently, most modded + /// multiplayer servers do not use encryption. + /// [JsonProperty("use_ssl")] - private bool _useSsl - { - get - { - return useSsl; - } + public bool useSsl { get; set; } - set - { - useSsl = value; - } - } + /// + /// Information only. Master server display name. + /// + [JsonProperty("name")] + public string? name { get; set; } + + /// + /// Information only. Master server display description. + /// + [JsonProperty("description")] + public string? description { get; set; } + + /// + /// Information only. Master server display image URL. + /// + [JsonProperty("image_url")] + public string? imageUrl { get; set; } + + /// + /// Information only. Maximum player count when creating new lobbies. + /// + [JsonProperty("max_players")] + public int maxPlayers { get; set; } - public RequiredMod[] requiredMods = null!; - public string maximumAppVersion = null!; /// - /// Request SSL (DTLS) connections when connecting to dedicated servers. - /// MultiplayerCore does NOT enforce this setting. + /// Information only. Server capability: per-player modifiers. /// - public bool useSsl = false; + [JsonProperty("supports_pp_modifiers")] + public bool supportsPPModifiers { get; set; } + + /// + /// Information only. Server capability: per-player difficulties. + /// + [JsonProperty("supports_pp_difficulties")] + public bool supportsPPDifficulties { get; set; } + + /// + /// Information only. Server capability: per-player level selection. + /// + [JsonProperty("supports_pp_maps")] + public bool supportsPPMaps { get; set; } [Serializable] public class RequiredMod @@ -63,15 +77,16 @@ public class RequiredMod /// BSIPA Mod ID. /// public string id = null!; + /// - /// Minimum version of the mod required. + /// Minimum version of the mod required, if installed. /// public string version = null!; + /// - /// Indicates whether the mod is required or not. - /// If false, only minimum versions are enforced. + /// Indicates whether the mod must be installed. /// public bool required = false; } } -} +} \ No newline at end of file From fcc35db110d2b9258a5561e92c2ad8ff312b8576 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Fri, 5 Apr 2024 02:47:29 +0200 Subject: [PATCH 04/75] wip(1.35): updated / consistent custom MUR error messages --- MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs b/MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs index 4033e53..e764b70 100644 --- a/MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs +++ b/MultiplayerCore/Patches/MultiplayerUnavailableReasonPatches.cs @@ -64,7 +64,7 @@ private static bool LocalizeMultiplayerUnavailableReason(MultiplayerUnavailableR if (multiplayerUnavailableReason == (MultiplayerUnavailableReason)5) { var metadata = PluginManager.GetPluginFromId(_requiredMod); - __result = $"Multiplayer Unavailable\nMod {metadata.Name} is out of date.\nPlease update to version {_requiredVersion} or newer."; + __result = $"Multiplayer Unavailable\nMod {metadata.Name} is missing or out of date\nPlease install version {_requiredVersion} or newer"; return false; } else if (multiplayerUnavailableReason == (MultiplayerUnavailableReason)6) { From 6d35862129d49617f7b7f89d7187cc1de4bec126 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Sat, 6 Apr 2024 00:16:57 +0200 Subject: [PATCH 05/75] wip(1.35): mark statusUrl as nullable in NetworkConfigPatcher --- MultiplayerCore/Patchers/NetworkConfigPatcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Patchers/NetworkConfigPatcher.cs b/MultiplayerCore/Patchers/NetworkConfigPatcher.cs index f67429e..f05d212 100644 --- a/MultiplayerCore/Patchers/NetworkConfigPatcher.cs +++ b/MultiplayerCore/Patchers/NetworkConfigPatcher.cs @@ -37,7 +37,7 @@ internal NetworkConfigPatcher( /// /// Override official servers with a custom API server. /// - public void UseCustomApiServer(string graphUrl, string statusUrl, int? maxPartySize = null, + public void UseCustomApiServer(string graphUrl, string? statusUrl, int? maxPartySize = null, string? quickPlaySetupUrl = null, bool disableSsl = true) { _logger.Debug($"Overriding multiplayer API server (graphUrl={graphUrl}, statusUrl={statusUrl}, " + @@ -46,7 +46,7 @@ public void UseCustomApiServer(string graphUrl, string statusUrl, int? maxPartyS GraphUrl = graphUrl; MasterServerStatusUrl = statusUrl; MaxPartySize = maxPartySize; - QuickPlaySetupUrl = quickPlaySetupUrl ?? statusUrl + "/mp_override.json"; + QuickPlaySetupUrl = quickPlaySetupUrl ?? (statusUrl != null ? statusUrl + "/mp_override.json" : null); DisableSsl = disableSsl; } From 2fe2d61f4cbacd19663914d2348c16851dc3b3a4 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Sat, 6 Apr 2024 21:41:05 +0200 Subject: [PATCH 06/75] wip(1.35): level loader - don't wait for non-players, quest consistency --- MultiplayerCore/Objects/MpLevelLoader.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index 4c6711e..58820b2 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -76,8 +76,11 @@ internal MpLevelLoader( // Ready check: player returned OK entitlement (load finished) OR already transitioned to gameplay var allPlayersReady = _sessionManager.connectedPlayers.All(p => - _entitlementChecker.GetKnownEntitlement(p.userId, levelId) == EntitlementsStatus.Ok || - p.HasState("in_gameplay")); + _entitlementChecker.GetKnownEntitlement(p.userId, levelId) == EntitlementsStatus.Ok // level loaded + || p.HasState("in_gameplay") // already playing + || p.HasState("backgrounded") // not actively in game + || !p.HasState("wants_to_play_next_level") // doesn't want to play (spectator) + ); if (!allPlayersReady) return; From 0982fe621397225525d4dc4dd9b7fa75e641f2a2 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Sun, 7 Apr 2024 01:42:51 +0200 Subject: [PATCH 07/75] wip(1.35): level loader - fix flipped countdown time logic --- MultiplayerCore/Objects/MpLevelLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index 58820b2..4cd3df8 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -71,7 +71,7 @@ internal MpLevelLoader( // Loader: level is loaded locally, waiting for countdown to transition to level // Modded behavior: wait until all players are ready before we transition - if (_startTime <= _sessionManager.syncTime) + if (_sessionManager.syncTime < _startTime) return; // Ready check: player returned OK entitlement (load finished) OR already transitioned to gameplay From 52a0417922881da9afed17f2e434faf9d652d6f8 Mon Sep 17 00:00:00 2001 From: RedBrumbler Date: Fri, 12 Apr 2024 14:40:50 +0200 Subject: [PATCH 08/75] Setup code to make the requirement UI work again with non-downloaded maps, also setup code to make the right list and main levelbar work with maps from beatmap packets --- .../Providers/MpBeatmapLevelProvider.cs | 27 +- MultiplayerCore/Installers/MpMenuInstaller.cs | 5 +- MultiplayerCore/Objects/MpPlayersDataModel.cs | 18 +- .../Patchers/BeatmapSelectionViewPatcher.cs | 49 ++++ .../GameServerPlayerTableCellPatcher.cs | 35 +++ MultiplayerCore/UI/MpColorsUI.cs | 52 +++- MultiplayerCore/UI/MpRequirementsUI.cs | 275 ++++++++++++------ 7 files changed, 349 insertions(+), 112 deletions(-) create mode 100644 MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs create mode 100644 MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs diff --git a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs index 5f4bae4..5d4e98c 100644 --- a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs +++ b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Threading.Tasks; +using System.Windows.Forms; using BeatSaverSharp; using MultiplayerCore.Beatmaps.Abstractions; using MultiplayerCore.Beatmaps.Packets; @@ -9,6 +12,8 @@ namespace MultiplayerCore.Beatmaps.Providers public class MpBeatmapLevelProvider { private readonly BeatSaver _beatsaver; + private readonly Dictionary _hashToNetworkMaps = new(); + private readonly Dictionary _hashToBeatsaverMaps = new(); internal MpBeatmapLevelProvider( UBinder beatsaver) @@ -46,11 +51,16 @@ internal MpBeatmapLevelProvider( /// An with a matching level hash, or null if none was found. public async Task GetBeatmapFromBeatSaver(string levelHash) { + if (_hashToBeatsaverMaps.TryGetValue(levelHash, out var map)) return map; var beatmap = await _beatsaver.BeatmapByHash(levelHash); - if (beatmap == null) - return null; - - return new BeatSaverBeatmapLevel(levelHash, beatmap); + if (beatmap != null) + { + map = new BeatSaverBeatmapLevel(levelHash, beatmap); + _hashToBeatsaverMaps.Add(levelHash, map); + return map; + } + + return null; } /// @@ -59,6 +69,11 @@ internal MpBeatmapLevelProvider( /// The packet to get preview data from /// An with a cover from BeatSaver. public MpBeatmap GetBeatmapFromPacket(MpBeatmapPacket packet) - => new NetworkBeatmapLevel(packet, _beatsaver); + { + if (_hashToNetworkMaps.TryGetValue(packet.levelHash, out var map)) return map; + map = new NetworkBeatmapLevel(packet); + _hashToNetworkMaps.Add(packet.levelHash, map); + return map; + } } } diff --git a/MultiplayerCore/Installers/MpMenuInstaller.cs b/MultiplayerCore/Installers/MpMenuInstaller.cs index 44f9ac9..923800c 100644 --- a/MultiplayerCore/Installers/MpMenuInstaller.cs +++ b/MultiplayerCore/Installers/MpMenuInstaller.cs @@ -1,4 +1,5 @@ -using MultiplayerCore.UI; +using MultiplayerCore.Patchers; +using MultiplayerCore.UI; using Zenject; namespace MultiplayerCore.Installers @@ -10,6 +11,8 @@ public override void InstallBindings() Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); + Container.BindInterfacesAndSelfTo().AsSingle(); + Container.BindInterfacesAndSelfTo().AsSingle(); // Inject sira stuff that didn't get injected on appinit Container.Inject(Container.Resolve()); diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index 889511c..5914181 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using MultiplayerCore.Beatmaps.Packets; @@ -14,6 +16,8 @@ internal class MpPlayersDataModel : LobbyPlayersDataModel, ILobbyPlayersDataMode private readonly MpPacketSerializer _packetSerializer; private readonly MpBeatmapLevelProvider _beatmapLevelProvider; private readonly SiraLog _logger; + private readonly Dictionary _lastPlayerBeatmapPackets = new(); + public IReadOnlyDictionary PlayerPackets => _lastPlayerBeatmapPackets; internal MpPlayersDataModel( MpPacketSerializer packetSerializer, @@ -67,8 +71,8 @@ private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer var beatmap = _beatmapLevelProvider.GetBeatmapFromPacket(packet); var characteristic = _beatmapCharacteristicCollection.GetBeatmapCharacteristicBySerializedName(packet.characteristicName); - - // TODO: How can we present our custom data in the base game UI? + + PutPlayerPacket(player.userId, packet); base.SetPlayerBeatmapLevel(player.userId, new BeatmapKey(beatmap.LevelID, characteristic, packet.difficulty)); } @@ -107,5 +111,15 @@ private async Task SendMpBeatmapPacket(BeatmapKey beatmapKey) var packet = new MpBeatmapPacket(levelData, beatmapKey); _multiplayerSessionManager.Send(packet); } + + public MpBeatmapPacket? GetPlayerPacket(string playerId) + { + _lastPlayerBeatmapPackets.TryGetValue(playerId, out var packet); + return packet; + } + + private void PutPlayerPacket(string playerId, MpBeatmapPacket packet) => _lastPlayerBeatmapPackets[playerId] = packet; + public MpBeatmapPacket? FindLevelPacket(string levelHash) => _lastPlayerBeatmapPackets.Values.FirstOrDefault(packet => packet.levelHash == levelHash); + } } diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs new file mode 100644 index 0000000..48d47f9 --- /dev/null +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -0,0 +1,49 @@ +using JetBrains.Annotations; +using MultiplayerCore.Beatmaps.Providers; +using MultiplayerCore.Objects; +using MultiplayerCore.UI; +using SiraUtil.Affinity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Zenject; + +namespace MultiplayerCore.Patchers +{ + internal class BeatmapSelectionViewPatcher : IAffinity + { + private MpPlayersDataModel _mpPlayersDataModel; + private MpBeatmapLevelProvider _mpBeatmapLevelProvider; + + + BeatmapSelectionViewPatcher(MpPlayersDataModel mpPlayersDataModel, MpBeatmapLevelProvider mpBeatmapLevelProvider) + { + _mpPlayersDataModel = mpPlayersDataModel; + _mpBeatmapLevelProvider = mpBeatmapLevelProvider; + } + + [AffinityPrefix] + [AffinityPatch(typeof(EditableBeatmapSelectionView), nameof(EditableBeatmapSelectionView.SetBeatmap))] + public bool EditableBeatmapSelectionView_SetBeatmap(EditableBeatmapSelectionView ___instance, in BeatmapKey beatmapKey) + { + if (!beatmapKey.IsValid()) return true; + + var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); + if (String.IsNullOrWhiteSpace(levelHash)) return true; + + var packet = _mpPlayersDataModel.FindLevelPacket(levelHash!); + if (packet == null) return true; + + ___instance._clearButton.gameObject.SetActive(___instance.showClearButton); + ___instance._noLevelText.enabled = false; + ___instance._levelBar.hide = false; + + // TODO: create a level to provide to the levelbar, on quest the beatmaplevelprovider actually provides game BeatmapLevels + // var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet) + // ___instance._levelBar.Setup(level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty); + return false; + } + } +} diff --git a/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs b/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs new file mode 100644 index 0000000..1f3c0d8 --- /dev/null +++ b/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs @@ -0,0 +1,35 @@ +using SiraUtil.Affinity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MultiplayerCore.UI; +using MultiplayerCore.Objects; +using Zenject; + +namespace MultiplayerCore.Patchers +{ + internal class GameServerPlayerTableCellPatcher : IAffinity + { + private MpPlayersDataModel _mpPlayersDataModel; + + GameServerPlayerTableCellPatcher(MpPlayersDataModel mpPlayersDataModel) => _mpPlayersDataModel = mpPlayersDataModel; + + [AffinityPatch(typeof(GameServerPlayerTableCell), nameof(GameServerPlayerTableCell.SetData))] + void GameServerPlayerTableCell_SetData(GameServerPlayerTableCell ___instance, IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData) + { + var beatmapKey = playerData.beatmapKey; + if (!beatmapKey.IsValid()) return; + + var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); + if (string.IsNullOrEmpty(levelHash)) return; + + var packet = _mpPlayersDataModel.PlayerPackets[connectedPlayer.userId]; + if (packet == null || packet.levelHash != levelHash) return; + + ___instance._suggestedLevelText.text = packet.songName; + } + + } +} diff --git a/MultiplayerCore/UI/MpColorsUI.cs b/MultiplayerCore/UI/MpColorsUI.cs index 1a93695..3ae1b1c 100644 --- a/MultiplayerCore/UI/MpColorsUI.cs +++ b/MultiplayerCore/UI/MpColorsUI.cs @@ -8,6 +8,7 @@ using HMUI; using MultiplayerCore.Beatmaps.Serializable; using MultiplayerCore.Helpers; +using SongCore.Data; using UnityEngine; namespace MultiplayerCore.UI @@ -32,11 +33,7 @@ internal class MpColorsUI : NotifiableBase [UIComponent("obstacleColorsToggle")] private ToggleSetting obstacleColorsToggle; - internal MpColorsUI( - LobbySetupViewController lobbySetupViewController) - { - _lobbySetupViewController = lobbySetupViewController; - } + internal MpColorsUI(LobbySetupViewController lobbySetupViewController) => _lobbySetupViewController = lobbySetupViewController; [UIComponent("modal")] private readonly ModalView _modal = null!; @@ -67,11 +64,10 @@ public bool EnvironmentColors set => SongCoreConfig.CustomSongEnvironmentColors = value; } - internal void ShowColors(DifficultyColors colors) + internal void ShowColors() { Parse(); _modal.Show(true); - SetColors(colors); // We do this to apply any changes to the toggles that might have been made from within SongCores UI noteColorToggle.Value = SongCoreConfig.CustomSongNoteColors; @@ -82,7 +78,7 @@ internal void ShowColors(DifficultyColors colors) private void Parse() { if (!_modal) - BSMLParser.instance.Parse(BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _lobbySetupViewController.GetComponentInChildren().gameObject, this); + BSMLParser.instance.Parse(BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _lobbySetupViewController.GetComponentInChildren(true).gameObject, this); _modal.transform.localPosition = _modalPosition; } @@ -96,11 +92,12 @@ private void PostParse() _modal.blockerClickedEvent += Dismiss; } - private void Dismiss() - => _modal.Hide(false, dismissedEvent); + private void Dismiss() => _modal.Hide(false, dismissedEvent); - private void SetColors(DifficultyColors colors) + internal void AcceptColors(DifficultyColors colors) { + Parse(); + Color saberLeft = colors.ColorLeft == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.ColorLeft); Color saberRight = colors.ColorRight == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.ColorRight); Color envLeft = colors.EnvColorLeft == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.EnvColorLeft); @@ -109,7 +106,38 @@ private void SetColors(DifficultyColors colors) Color envRightBoost = colors.EnvColorRightBoost == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.EnvColorRightBoost); Color obstacle = colors.ObstacleColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(colors.ObstacleColor); - colorSchemeView.SetColors(saberLeft, saberRight, envLeft, envRight, envLeftBoost, envRightBoost, obstacle); + colorSchemeView.SetColors( + saberLeft, + saberRight, + envLeft, + envRight, + envLeftBoost, + envRightBoost, + obstacle + ); + } + + internal void AcceptColors(ExtraSongData.MapColor? leftColor, ExtraSongData.MapColor? rightColor, ExtraSongData.MapColor? envLeftColor, ExtraSongData.MapColor? envLeftBoostColor, ExtraSongData.MapColor? envRightColor, ExtraSongData.MapColor? envRightBoostColor, ExtraSongData.MapColor? obstacleColor) + { + Parse(); + + Color saberLeft = leftColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(leftColor); + Color saberRight = rightColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(rightColor); + Color envLeft = envLeftColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(envLeftColor); + Color envRight = envRightColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(envRightColor); + Color envLeftBoost = envLeftBoostColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(envLeftBoostColor); + Color envRightBoost = envRightBoostColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(envRightBoostColor); + Color obstacle = obstacleColor == null ? voidColor : SongCore.Utilities.Utils.ColorFromMapColor(obstacleColor); + + colorSchemeView.SetColors( + saberLeft, + saberRight, + envLeft, + envRight, + envLeftBoost, + envRightBoost, + obstacle + ); } } } diff --git a/MultiplayerCore/UI/MpRequirementsUI.cs b/MultiplayerCore/UI/MpRequirementsUI.cs index 66204c8..199cb2f 100644 --- a/MultiplayerCore/UI/MpRequirementsUI.cs +++ b/MultiplayerCore/UI/MpRequirementsUI.cs @@ -1,13 +1,19 @@ using System; +using System.Collections.Generic; using System.Reflection; +using System.Runtime.CompilerServices; using BeatSaberMarkupLanguage; using BeatSaberMarkupLanguage.Attributes; using BeatSaberMarkupLanguage.Components; using HMUI; using IPA.Utilities; +using MultiplayerCore.Beatmaps.Packets; +using MultiplayerCore.Objects; using SiraUtil.Logging; using UnityEngine; +using UnityEngine.UIElements; using Zenject; +using static IPA.Logging.Logger; namespace MultiplayerCore.UI { @@ -33,17 +39,23 @@ internal class MpRequirementsUI : NotifiableBase, IInitializable, IDisposable private readonly LobbySetupViewController _lobbySetupViewController; private readonly ILobbyPlayersDataModel _playersDataModel; private readonly MpColorsUI _colorsUI; + private readonly BeatmapLevelsModel _beatmapLevelsModel; private readonly SiraLog _logger; + private readonly List _unusedCells; + private readonly List _levelInfoCells; + internal MpRequirementsUI( LobbySetupViewController lobbySetupViewController, ILobbyPlayersDataModel playersDataModel, MpColorsUI colorsUI, + BeatmapLevelsModel beatmapLevelsModel, SiraLog logger) { _lobbySetupViewController = lobbySetupViewController; _playersDataModel = playersDataModel; _colorsUI = colorsUI; + _beatmapLevelsModel = beatmapLevelsModel; _logger = logger; } @@ -95,6 +107,9 @@ public void Initialize() _playersDataModel.didChangeEvent += BeatmapSelected; _colorsUI.dismissedEvent += ColorsDismissed; + + BSMLParser.instance.Parse(BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _root.gameObject, this); + _modalPosition = _modal!.transform.localPosition; } public void Dispose() @@ -103,94 +118,181 @@ public void Dispose() _colorsUI.dismissedEvent -= ColorsDismissed; } - private void BeatmapSelected(string a) + private void BeatmapSelected(string _) { - var beatmapLevel = _playersDataModel[_playersDataModel.localUserId].beatmapKey; - // TODO: Load MpBeatmap data from somewhere - // if (beatmapLevel?.beatmapLevel is MpBeatmap mpLevel) - // { - // string characteristicName = null!; - // if (mpLevel.DifficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.name)) - // characteristicName = beatmapLevel.beatmapCharacteristic.name; - // else if (mpLevel.DifficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName)) - // characteristicName = beatmapLevel.beatmapCharacteristic.serializedName; - // if (characteristicName != null && mpLevel.DifficultyColors[characteristicName].TryGetValue(beatmapLevel.beatmapDifficulty, out var colors)) - // ButtonInteractable = colors.AnyAreNotNull; - // else - // ButtonInteractable = (characteristicName != null && mpLevel.Requirements[characteristicName].Any()) || (mpLevel.Contributors?.Any() ?? false); - // } - // else - ButtonInteractable = false; + var key = _playersDataModel[_playersDataModel.localUserId].beatmapKey; + if (!key.IsValid()) return; + + var levelId = key.levelId; + var localLevel = _beatmapLevelsModel.GetBeatmapLevel(levelId); + if (localLevel != null) // we have a local level to set info from + { + SetRequirementsFromLevel(localLevel, key); + return; + } + + if (_playersDataModel is MpPlayersDataModel mpPlayersDataModel) + { + var levelHash = Utilities.HashForLevelID(levelId); + var packet = mpPlayersDataModel.FindLevelPacket(levelHash); + if (packet != null) // we have a packet to set info from + { + SetRequirementsFromPacket(packet); + return; + } + } + + SetNoRequirementsFound(); // nothing found } - private void ColorsDismissed() - => ShowRequirements(); + private void SetRequirementsFromLevel(BeatmapLevel level, in BeatmapKey key) + { + ClearCells(_levelInfoCells); - [UIAction("button-click")] - internal void ShowRequirements() + var levelHash = Utilities.HashForLevelID(key.levelId); + if (!string.IsNullOrEmpty(levelHash)) + { + var extraSongData = SongCore.Collections.RetrieveExtraSongData(levelHash!); + if (extraSongData != null) + { + var diffData = SongCore.Collections.RetrieveDifficultyData(level, key); + if (diffData != null && diffData.additionalDifficultyData != null && diffData.additionalDifficultyData._requirements != null) + { + foreach (var req in diffData.additionalDifficultyData._requirements) + { + var cell = GetCellInfo(); + bool installed = SongCore.Collections.capabilities.Contains(req); + cell.text = $"{req}"; + cell.subtext = installed ? "Requirement found" : "Requirement missing"; + cell.icon = installed ? HaveReqIcon : MissingReqIcon; + _levelInfoCells.Add(cell); + } + } + + foreach (var contributor in extraSongData.contributors) + { + var cell = GetCellInfo(); + cell.text = $"{contributor._name}"; + cell.subtext = contributor._role; + cell.icon = InfoIcon; + _levelInfoCells.Add(cell); + } + + if (diffData != null && + !( // check all colors for null, if all are null just ignore + diffData._colorLeft != null || + diffData._colorRight != null || + diffData._envColorLeft != null || + diffData._envColorLeftBoost != null || + diffData._envColorRight != null || + diffData._envColorRightBoost != null || + diffData._obstacleColor != null + ) + ) + { + var cell = GetCellInfo(); + cell.text = "Custom Colors Available"; + cell.subtext = "Click here to preview"; + cell.icon = ColorsIcon; + _levelInfoCells.Add(cell); + + _colorsUI.AcceptColors( + diffData._colorLeft, + diffData._colorRight, + diffData._envColorLeft, + diffData._envColorLeftBoost, + diffData._envColorRight, + diffData._envColorRightBoost, + diffData._obstacleColor + ); + } + } + } + + UpdateData(); + } + + private void SetRequirementsFromPacket(MpBeatmapPacket packet) { - if (_modal == null) + ClearCells(_levelInfoCells); + + var diff = packet.difficulty; + foreach (var req in packet.requirements[diff]) { - BSMLParser.instance.Parse(BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _root.gameObject, this); - _modalPosition = _modal!.transform.localPosition; + var cell = GetCellInfo(); + bool installed = SongCore.Collections.capabilities.Contains(req); + cell.text = $"{req}"; + cell.subtext = installed ? "Requirement found" : "Requirement missing"; + cell.icon = installed ? HaveReqIcon : MissingReqIcon; + _levelInfoCells.Add(cell); } - _modal.transform.localPosition = _modalPosition; - _modal.Show(true); + foreach (var contributor in packet.contributors) + { + var cell = GetCellInfo(); + cell.text = $"{contributor._name}"; + cell.subtext = contributor._role; + cell.icon = InfoIcon; + _levelInfoCells.Add(cell); + } + + if (packet.mapColors.TryGetValue(diff, out var mapColors) && mapColors.AnyAreNotNull) + { + var cell = GetCellInfo(); + cell.text = "Custom Colors Available"; + cell.subtext = "Click here to preview"; + cell.icon = ColorsIcon; + _levelInfoCells.Add(cell); + + _colorsUI.AcceptColors(mapColors); + } + + UpdateData(); + } + + private void SetNoRequirementsFound() + { + ClearCells(_levelInfoCells); + + UpdateData(); + } + + private void ClearCells(List cells) + { + foreach (var cell in cells) _unusedCells.Add(cell); + cells.Clear(); + } + + private CustomListTableData.CustomCellInfo GetCellInfo() + { + if (_unusedCells.Count == 0) return new CustomListTableData.CustomCellInfo(String.Empty, String.Empty, null); + var cell = _unusedCells[0]; + _unusedCells.RemoveAt(0); + return cell; + } + + private void UpdateData() + { customListTableData.data.Clear(); - var localUserId = _playersDataModel.localUserId; - if (!_playersDataModel.ContainsKey(localUserId)) - return; - var localPlayerDataModel = _playersDataModel[localUserId]; - var level = localPlayerDataModel.beatmapKey; - - // TODO: Load MpBeatmap data from somewhere - // if (level.beatmapLevel is MpBeatmap mpLevel) - // { - // string characteristicName = null!; - // if (mpLevel.Requirements.ContainsKey(level.beatmapCharacteristic.name) || mpLevel.DifficultyColors.ContainsKey(level.beatmapCharacteristic.name)) - // characteristicName = level.beatmapCharacteristic.name; - // else if (mpLevel.Requirements.ContainsKey(level.beatmapCharacteristic.serializedName) || mpLevel.DifficultyColors.ContainsKey(level.beatmapCharacteristic.serializedName)) - // characteristicName = level.beatmapCharacteristic.serializedName; - // - // // Requirements - // if (mpLevel.Requirements.TryGetValue(characteristicName, out var difficultiesRequirements)) - // if (difficultiesRequirements.TryGetValue(level.beatmapDifficulty, out var difficultyRequirements) && difficultyRequirements.Any()) - // foreach (string req in difficultyRequirements) - // customListTableData.data.Add(!SongCore.Collections.capabilities.Contains(req) - // ? new CustomCellInfo($"{req}", "Missing Requirement", MissingReqIcon) - // : new CustomCellInfo($"{req}", "Requirement", HaveReqIcon)); - // - // // Contributors - // if (mpLevel.Contributors != null) - // foreach (var contributor in mpLevel.Contributors) - // { - // if (contributor.icon == null) - // { - // if (!string.IsNullOrWhiteSpace(contributor._iconPath) && !string.IsNullOrEmpty(contributor._iconPath) && SongCore.Collections.songWithHashPresent(mpLevel.LevelHash)) - // { - // var songCoreLevel = SongCore.Loader.GetLevelByHash(mpLevel.LevelHash); - // contributor.icon = SongCore.Utilities.Utils.LoadSpriteFromFile(Path.Combine(songCoreLevel!.customLevelPath, contributor._iconPath)); - // customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, contributor.icon != null ? contributor.icon : InfoIcon)); - // } - // else - // customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, InfoIcon)); - // } - // else - // customListTableData.data.Add(new CustomCellInfo(contributor._name, contributor._role, contributor.icon)); - // } - // - // // Colors - // var customColorsEnabled = SongCoreConfig.AnyCustomSongColors; - // if (mpLevel.DifficultyColors.TryGetValue(characteristicName, out var difficultyColors) && difficultyColors.TryGetValue(level.beatmapDifficulty, out var colors) && (colors.AnyAreNotNull)) - // customListTableData.data.Add(new CustomCellInfo($"Custom Colors Available", $"Click here to preview & {(customColorsEnabled ? "disable" : "enable")} it.", ColorsIcon)); - // else if (mpLevel is BeatSaverBeatmapLevel) - // customListTableData.data.Add(new CustomCellInfo($"Custom Colors", $"Click here to preview & {(customColorsEnabled ? "disable" : "enable")} it.", ColorsIcon)); - // - // customListTableData.tableView.ReloadData(); - // customListTableData.tableView.ScrollToCellWithIdx(0, TableView.ScrollPositionType.Beginning, false); - // } + foreach (var cell in _levelInfoCells) customListTableData.data.Add(cell); + + customListTableData.tableView.ReloadData(); + customListTableData.tableView.ScrollToCellWithIdx(0, TableView.ScrollPositionType.Beginning, false); + + UpdateRequirementButton(); + } + + // if there is data, we should have the button be active + private void UpdateRequirementButton() => ButtonInteractable = customListTableData.data.Count > 0; + + private void ColorsDismissed() => ShowRequirements(); + + [UIAction("button-click")] + internal void ShowRequirements() + { + _modal.transform.localPosition = _modalPosition; + _modal.Show(true); } [UIAction("list-select")] @@ -199,19 +301,10 @@ private void Select(TableView _, int index) var localUserData = _playersDataModel[_playersDataModel.localUserId]; var beatmapLevel = localUserData.beatmapKey; - // TODO: Load MpBeatmap data from somewhere - // if (beatmapLevel.beatmapLevel is MpBeatmap mpLevel) - // { - // string characteristicName = null!; - // if (mpLevel.Requirements.ContainsKey(beatmapLevel.beatmapCharacteristic.name) || mpLevel.DifficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.name)) - // characteristicName = beatmapLevel.beatmapCharacteristic.name; - // else if (mpLevel.Requirements.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName) || mpLevel.DifficultyColors.ContainsKey(beatmapLevel.beatmapCharacteristic.serializedName)) - // characteristicName = beatmapLevel.beatmapCharacteristic.serializedName; - // - // customListTableData.tableView.ClearSelection(); - // if (customListTableData.data[index].icon == ColorsIcon) - // _modal.Hide(false, () => _colorsUI.ShowColors(mpLevel.DifficultyColors[characteristicName][beatmapLevel.beatmapDifficulty])); - // } + var cell = customListTableData.data[index]; + if (cell.icon == ColorsIcon) _modal.Hide(false, () => _colorsUI.ShowColors()); + + customListTableData.tableView.ClearSelection(); } } } From cbf92ef7447bb058bf4dc99177f0b6db4bd73805 Mon Sep 17 00:00:00 2001 From: RedBrumbler Date: Tue, 16 Apr 2024 01:53:57 +0200 Subject: [PATCH 09/75] Fix null list --- MultiplayerCore/UI/MpRequirementsUI.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/MultiplayerCore/UI/MpRequirementsUI.cs b/MultiplayerCore/UI/MpRequirementsUI.cs index 199cb2f..213cd1d 100644 --- a/MultiplayerCore/UI/MpRequirementsUI.cs +++ b/MultiplayerCore/UI/MpRequirementsUI.cs @@ -42,8 +42,8 @@ internal class MpRequirementsUI : NotifiableBase, IInitializable, IDisposable private readonly BeatmapLevelsModel _beatmapLevelsModel; private readonly SiraLog _logger; - private readonly List _unusedCells; - private readonly List _levelInfoCells; + private readonly List _unusedCells = new(); + private readonly List _levelInfoCells = new(); internal MpRequirementsUI( LobbySetupViewController lobbySetupViewController, @@ -125,24 +125,24 @@ private void BeatmapSelected(string _) var levelId = key.levelId; var localLevel = _beatmapLevelsModel.GetBeatmapLevel(levelId); - if (localLevel != null) // we have a local level to set info from - { - SetRequirementsFromLevel(localLevel, key); - return; + if (localLevel != null) // we have a local level to set info from + { + SetRequirementsFromLevel(localLevel, key); + return; } if (_playersDataModel is MpPlayersDataModel mpPlayersDataModel) { var levelHash = Utilities.HashForLevelID(levelId); - var packet = mpPlayersDataModel.FindLevelPacket(levelHash); + var packet = mpPlayersDataModel.FindLevelPacket(levelHash); if (packet != null) // we have a packet to set info from { - SetRequirementsFromPacket(packet); + SetRequirementsFromPacket(packet); return; - } - } - - SetNoRequirementsFound(); // nothing found + } + } + + SetNoRequirementsFound(); // nothing found } private void SetRequirementsFromLevel(BeatmapLevel level, in BeatmapKey key) From 7a44bb967af96e264035df969089e8f982738efe Mon Sep 17 00:00:00 2001 From: RedBrumbler Date: Mon, 29 Apr 2024 16:58:49 +0200 Subject: [PATCH 10/75] More fixes and workarounds for problems with song lists --- .../Beatmaps/BeatSaverPreviewMediaData.cs | 62 ++++ .../Providers/MpBeatmapLevelProvider.cs | 2 + MultiplayerCore/MultiplayerCore.csproj | 51 ++-- MultiplayerCore/Objects/MpPlayersDataModel.cs | 269 ++++++++++-------- .../Patchers/BeatmapSelectionViewPatcher.cs | 92 ++++-- .../GameServerPlayerTableCellPatcher.cs | 83 +++++- 6 files changed, 386 insertions(+), 173 deletions(-) create mode 100644 MultiplayerCore/Beatmaps/BeatSaverPreviewMediaData.cs diff --git a/MultiplayerCore/Beatmaps/BeatSaverPreviewMediaData.cs b/MultiplayerCore/Beatmaps/BeatSaverPreviewMediaData.cs new file mode 100644 index 0000000..d9a3db5 --- /dev/null +++ b/MultiplayerCore/Beatmaps/BeatSaverPreviewMediaData.cs @@ -0,0 +1,62 @@ +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BeatSaverSharp; +using BeatSaverSharp.Models; + +namespace MultiplayerCore.Beatmaps +{ + public class BeatSaverPreviewMediaData : IPreviewMediaData + { + + public string LevelHash { get; private set; } + public BeatSaver BeatSaverClient { get; private set; } + public Sprite? CoverImagesprite { get; private set; } + + public BeatSaverPreviewMediaData(string levelHash) : this(Plugin._beatsaver, levelHash) {} + + public BeatSaverPreviewMediaData(BeatSaver beatsaver, string levelHash) + { + BeatSaverClient = beatsaver; + LevelHash = levelHash; + } + + private Beatmap? _beatmap = null; + private async Task GetBeatsaverBeatmap() + { + if (_beatmap != null) return _beatmap; + _beatmap = await BeatSaverClient.BeatmapByHash(LevelHash); + return _beatmap; + } + + public async Task GetCoverSpriteAsync(CancellationToken cancellationToken) + { + if (CoverImagesprite != null) return CoverImagesprite; + + var bm = await GetBeatsaverBeatmap(); + if (bm == null) return null!; + + byte[]? coverBytes = await bm.LatestVersion.DownloadCoverImage(cancellationToken); + if (coverBytes == null || coverBytes.Length == 0) return null!; + + Texture2D texture = new Texture2D(2, 2); + texture.LoadImage(coverBytes); + CoverImagesprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0, 0), 100.0f); + return CoverImagesprite; + } + + public async Task GetPreviewAudioClip(CancellationToken cancellationToken) + { + // TODO: something with preview url + // var bm = await GetBeatsaverBeatmap(); + // bm.LatestVersion.PreviewURL + return null; + } + + public void UnloadPreviewAudioClip() {} + } +} diff --git a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs index 5d4e98c..d0f4bed 100644 --- a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs +++ b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs @@ -63,6 +63,8 @@ internal MpBeatmapLevelProvider( return null; } + public BeatSaverPreviewMediaData MakeBeatSaverPreviewMediaData(string levelHash) => new BeatSaverPreviewMediaData(_beatsaver, levelHash); + /// /// Gets an from the information in the provided packet. /// diff --git a/MultiplayerCore/MultiplayerCore.csproj b/MultiplayerCore/MultiplayerCore.csproj index ef43d2f..86e16dc 100644 --- a/MultiplayerCore/MultiplayerCore.csproj +++ b/MultiplayerCore/MultiplayerCore.csproj @@ -43,6 +43,11 @@ + + $(BeatSaberDir)\Beat Saber_Data\Managed\AdditionalContentModel.Interfaces.dll + False + False + $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll False @@ -51,10 +56,10 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.Polyglot.dll - - $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.UnityExtension.dll - False - + + $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.UnityExtension.dll + False + $(BeatSaberDir)\Beat Saber_Data\Managed\Ignorance.dll False @@ -120,19 +125,19 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\Menu.ColorSettings.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\Networking.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\PlatformUserModel.dll - False - - + + $(BeatSaberDir)\Beat Saber_Data\Managed\Menu.ColorSettings.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\Networking.dll + False + + + $(BeatSaberDir)\Beat Saber_Data\Managed\PlatformUserModel.dll + False + + $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll False @@ -174,11 +179,11 @@ False False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.JSONSerializeModule.dll - False + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.JSONSerializeModule.dll + False - + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll @@ -252,7 +257,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -274,4 +279,4 @@ - + \ No newline at end of file diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index 5914181..dcbc52e 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -1,125 +1,144 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using JetBrains.Annotations; -using MultiplayerCore.Beatmaps.Packets; -using MultiplayerCore.Beatmaps.Providers; -using MultiplayerCore.Networking; -using SiraUtil.Logging; - -namespace MultiplayerCore.Objects -{ - [UsedImplicitly] - internal class MpPlayersDataModel : LobbyPlayersDataModel, ILobbyPlayersDataModel, IDisposable - { - private readonly MpPacketSerializer _packetSerializer; - private readonly MpBeatmapLevelProvider _beatmapLevelProvider; - private readonly SiraLog _logger; - private readonly Dictionary _lastPlayerBeatmapPackets = new(); - public IReadOnlyDictionary PlayerPackets => _lastPlayerBeatmapPackets; - - internal MpPlayersDataModel( - MpPacketSerializer packetSerializer, - MpBeatmapLevelProvider beatmapLevelProvider, - SiraLog logger) - { - _packetSerializer = packetSerializer; - _beatmapLevelProvider = beatmapLevelProvider; - _logger = logger; - } - - public new void Activate() - { - _packetSerializer.RegisterCallback(HandleMpCoreBeatmapPacket); - base.Activate(); - _menuRpcManager.getRecommendedBeatmapEvent -= base.HandleMenuRpcManagerGetRecommendedBeatmap; - _menuRpcManager.getRecommendedBeatmapEvent += this.HandleMenuRpcManagerGetRecommendedBeatmap; - _menuRpcManager.recommendBeatmapEvent -= base.HandleMenuRpcManagerRecommendBeatmap; - _menuRpcManager.recommendBeatmapEvent += this.HandleMenuRpcManagerRecommendBeatmap; - } - - public new void Deactivate() - { - _packetSerializer.UnregisterCallback(); - _menuRpcManager.getRecommendedBeatmapEvent -= this.HandleMenuRpcManagerGetRecommendedBeatmap; - _menuRpcManager.getRecommendedBeatmapEvent += base.HandleMenuRpcManagerGetRecommendedBeatmap; - _menuRpcManager.recommendBeatmapEvent -= this.HandleMenuRpcManagerRecommendBeatmap; - _menuRpcManager.recommendBeatmapEvent += base.HandleMenuRpcManagerRecommendBeatmap; - base.Deactivate(); - } - - public new void Dispose() - => Deactivate(); - - private new void SetPlayerBeatmapLevel(string userId, in BeatmapKey beatmapKey) - { - // Game: A player (can be the local player!) has selected / recommended a beatmap - - if (userId == _multiplayerSessionManager.localPlayer.userId) - // If local player: send extended beatmap info to other players - _ = SendMpBeatmapPacket(beatmapKey); - - base.SetPlayerBeatmapLevel(userId, in beatmapKey); - } - - private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer player) - { - // Packet: Another player has recommended a beatmap (MpCore), we have received details for the level preview - - _logger.Debug($"'{player.userId}' selected song '{packet.levelHash}'."); - - var beatmap = _beatmapLevelProvider.GetBeatmapFromPacket(packet); - var characteristic = _beatmapCharacteristicCollection.GetBeatmapCharacteristicBySerializedName(packet.characteristicName); - - PutPlayerPacket(player.userId, packet); - base.SetPlayerBeatmapLevel(player.userId, new BeatmapKey(beatmap.LevelID, characteristic, packet.difficulty)); - } - - private new void HandleMenuRpcManagerGetRecommendedBeatmap(string userId) - { - // RPC: The server / another player has asked us to send our recommended beatmap - - var selectedBeatmapKey = _playersData[localUserId].beatmapKey; - _ = SendMpBeatmapPacket(selectedBeatmapKey); - - base.HandleMenuRpcManagerGetRecommendedBeatmap(userId); - } - - private new void HandleMenuRpcManagerRecommendBeatmap(string userId, BeatmapKeyNetSerializable beatmapKeySerializable) - { - // RPC: Another player has recommended a beatmap (base game) - - if (!string.IsNullOrEmpty(Utilities.HashForLevelID(beatmapKeySerializable.levelID))) - return; - - base.HandleMenuRpcManagerRecommendBeatmap(userId, beatmapKeySerializable); - } - - private async Task SendMpBeatmapPacket(BeatmapKey beatmapKey) - { - var levelId = beatmapKey.levelId; - - var levelHash = Utilities.HashForLevelID(levelId); - if (levelHash == null) - return; - - var levelData = await _beatmapLevelProvider.GetBeatmap(levelHash); - if (levelData == null) - return; - - var packet = new MpBeatmapPacket(levelData, beatmapKey); - _multiplayerSessionManager.Send(packet); - } - - public MpBeatmapPacket? GetPlayerPacket(string playerId) - { - _lastPlayerBeatmapPackets.TryGetValue(playerId, out var packet); - return packet; - } - - private void PutPlayerPacket(string playerId, MpBeatmapPacket packet) => _lastPlayerBeatmapPackets[playerId] = packet; - public MpBeatmapPacket? FindLevelPacket(string levelHash) => _lastPlayerBeatmapPackets.Values.FirstOrDefault(packet => packet.levelHash == levelHash); - - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; +using MultiplayerCore.Beatmaps.Packets; +using MultiplayerCore.Beatmaps.Providers; +using MultiplayerCore.Networking; +using SiraUtil.Logging; + +namespace MultiplayerCore.Objects +{ + [UsedImplicitly] + internal class MpPlayersDataModel : LobbyPlayersDataModel, ILobbyPlayersDataModel, IDisposable + { + private readonly MpPacketSerializer _packetSerializer; + private readonly MpBeatmapLevelProvider _beatmapLevelProvider; + private readonly SiraLog _logger; + private readonly Dictionary _lastPlayerBeatmapPackets = new(); + public IReadOnlyDictionary PlayerPackets => _lastPlayerBeatmapPackets; + + internal MpPlayersDataModel( + MpPacketSerializer packetSerializer, + MpBeatmapLevelProvider beatmapLevelProvider, + SiraLog logger) + { + _packetSerializer = packetSerializer; + _beatmapLevelProvider = beatmapLevelProvider; + _logger = logger; + } + + public new void Activate() + { + _packetSerializer.RegisterCallback(HandleMpCoreBeatmapPacket); + base.Activate(); + _menuRpcManager.getRecommendedBeatmapEvent -= base.HandleMenuRpcManagerGetRecommendedBeatmap; + _menuRpcManager.getRecommendedBeatmapEvent += this.HandleMenuRpcManagerGetRecommendedBeatmap; + _menuRpcManager.recommendBeatmapEvent -= base.HandleMenuRpcManagerRecommendBeatmap; + _menuRpcManager.recommendBeatmapEvent += this.HandleMenuRpcManagerRecommendBeatmap; + } + + public new void Deactivate() + { + _packetSerializer.UnregisterCallback(); + _menuRpcManager.getRecommendedBeatmapEvent -= this.HandleMenuRpcManagerGetRecommendedBeatmap; + _menuRpcManager.getRecommendedBeatmapEvent += base.HandleMenuRpcManagerGetRecommendedBeatmap; + _menuRpcManager.recommendBeatmapEvent -= this.HandleMenuRpcManagerRecommendBeatmap; + _menuRpcManager.recommendBeatmapEvent += base.HandleMenuRpcManagerRecommendBeatmap; + base.Deactivate(); + } + + public new void Dispose() + => Deactivate(); + + private new void SetPlayerBeatmapLevel(string userId, in BeatmapKey beatmapKey) + { + // Game: A player (can be the local player!) has selected / recommended a beatmap + + if (userId == _multiplayerSessionManager.localPlayer.userId) + // If local player: send extended beatmap info to other players + _ = SendMpBeatmapPacket(beatmapKey); + + base.SetPlayerBeatmapLevel(userId, in beatmapKey); + } + + private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer player) + { + // Packet: Another player has recommended a beatmap (MpCore), we have received details for the level preview + + _logger.Debug($"'{player.userId}' selected song '{packet.levelHash}'."); + + var beatmap = _beatmapLevelProvider.GetBeatmapFromPacket(packet); + var characteristic = _beatmapCharacteristicCollection.GetBeatmapCharacteristicBySerializedName(packet.characteristicName); + + PutPlayerPacket(player.userId, packet); + base.SetPlayerBeatmapLevel(player.userId, new BeatmapKey(beatmap.LevelID, characteristic, packet.difficulty)); + } + + private new void HandleMenuRpcManagerGetRecommendedBeatmap(string userId) + { + // RPC: The server / another player has asked us to send our recommended beatmap + + var selectedBeatmapKey = _playersData[localUserId].beatmapKey; + _ = SendMpBeatmapPacket(selectedBeatmapKey); + + base.HandleMenuRpcManagerGetRecommendedBeatmap(userId); + } + + private new void HandleMenuRpcManagerRecommendBeatmap(string userId, BeatmapKeyNetSerializable beatmapKeySerializable) + { + // RPC: Another player has recommended a beatmap (base game) + + if (!string.IsNullOrEmpty(Utilities.HashForLevelID(beatmapKeySerializable.levelID))) + return; + + base.HandleMenuRpcManagerRecommendBeatmap(userId, beatmapKeySerializable); + } + + private async Task SendMpBeatmapPacket(BeatmapKey beatmapKey) + { + var levelId = beatmapKey.levelId; + _logger.Debug($"Sending beatmap packet for level {levelId}"); + + var levelHash = Utilities.HashForLevelID(levelId); + if (levelHash == null) + { + _logger.Debug("Not a custom level, returning..."); + return; + } + + var levelData = await _beatmapLevelProvider.GetBeatmap(levelHash); + if (levelData == null) + { + _logger.Debug("Could not get level data for beatmap, returning!"); + return; + } + + var packet = new MpBeatmapPacket(levelData, beatmapKey); + _logger.Debug("Actually sending packet"); + _multiplayerSessionManager.Send(packet); + } + + public MpBeatmapPacket? GetPlayerPacket(string playerId) + { + _lastPlayerBeatmapPackets.TryGetValue(playerId, out var packet); + _logger.Debug($"Got player packet for {playerId} with levelHash: {packet?.levelHash ?? "NULL"}"); + return packet; + } + + private void PutPlayerPacket(string playerId, MpBeatmapPacket packet) + { + _logger.Debug($"Putting packet for player {playerId} with levelHash: {packet.levelHash}"); + _lastPlayerBeatmapPackets[playerId] = packet; + } + + public MpBeatmapPacket? FindLevelPacket(string levelHash) + { + var packet = _lastPlayerBeatmapPackets.Values.FirstOrDefault(packet => packet.levelHash == levelHash); + _logger.Debug($"Found packet: {packet?.levelHash ?? "NULL"}"); + return packet; + } + + } +} diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs index 48d47f9..b2adee7 100644 --- a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -1,14 +1,11 @@ -using JetBrains.Annotations; +using MultiplayerCore.Beatmaps; +using MultiplayerCore.Beatmaps.Abstractions; +using MultiplayerCore.Beatmaps.Packets; using MultiplayerCore.Beatmaps.Providers; using MultiplayerCore.Objects; -using MultiplayerCore.UI; -using SiraUtil.Affinity; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Zenject; +using SiraUtil.Affinity; namespace MultiplayerCore.Patchers { @@ -16,19 +13,46 @@ internal class BeatmapSelectionViewPatcher : IAffinity { private MpPlayersDataModel _mpPlayersDataModel; private MpBeatmapLevelProvider _mpBeatmapLevelProvider; + private BeatmapLevelsModel _beatmapLevelsModel; - BeatmapSelectionViewPatcher(MpPlayersDataModel mpPlayersDataModel, MpBeatmapLevelProvider mpBeatmapLevelProvider) + BeatmapSelectionViewPatcher(ILobbyPlayersDataModel playersDataModel, MpBeatmapLevelProvider mpBeatmapLevelProvider, BeatmapLevelsModel beatmapLevelsModel) { - _mpPlayersDataModel = mpPlayersDataModel; + _mpPlayersDataModel = playersDataModel as MpPlayersDataModel; _mpBeatmapLevelProvider = mpBeatmapLevelProvider; + _beatmapLevelsModel = beatmapLevelsModel; } [AffinityPrefix] [AffinityPatch(typeof(EditableBeatmapSelectionView), nameof(EditableBeatmapSelectionView.SetBeatmap))] - public bool EditableBeatmapSelectionView_SetBeatmap(EditableBeatmapSelectionView ___instance, in BeatmapKey beatmapKey) + public bool EditableBeatmapSelectionView_SetBeatmap(ref EditableBeatmapSelectionView __instance, in BeatmapKey beatmapKey) + { + if (_mpPlayersDataModel == null) return false; + if (!beatmapKey.IsValid()) return true; + if (_beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId) != null) return true; + + var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); + if (String.IsNullOrWhiteSpace(levelHash)) return true; + + var packet = _mpPlayersDataModel.FindLevelPacket(levelHash!); + if (packet == null) return true; + + __instance._clearButton.gameObject.SetActive(__instance.showClearButton); + __instance._noLevelText.enabled = false; + __instance._levelBar.hide = false; + + var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); + __instance._levelBar.Setup(level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty); + return false; + } + + [AffinityPrefix] + [AffinityPatch(typeof(BeatmapSelectionView), nameof(BeatmapSelectionView.SetBeatmap))] + public bool BeatmapSelectionView_SetBeatmap(ref BeatmapSelectionView __instance, in BeatmapKey beatmapKey) { + if (_mpPlayersDataModel == null) return false; if (!beatmapKey.IsValid()) return true; + if (_beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId) != null) return true; var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); if (String.IsNullOrWhiteSpace(levelHash)) return true; @@ -36,14 +60,50 @@ public bool EditableBeatmapSelectionView_SetBeatmap(EditableBeatmapSelectionView var packet = _mpPlayersDataModel.FindLevelPacket(levelHash!); if (packet == null) return true; - ___instance._clearButton.gameObject.SetActive(___instance.showClearButton); - ___instance._noLevelText.enabled = false; - ___instance._levelBar.hide = false; + __instance._noLevelText.enabled = false; + __instance._levelBar.hide = false; - // TODO: create a level to provide to the levelbar, on quest the beatmaplevelprovider actually provides game BeatmapLevels - // var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet) - // ___instance._levelBar.Setup(level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty); + var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); + __instance._levelBar.Setup(level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty); return false; } } + + static internal class PacketExt + { + public static BeatmapLevel MakeBeatmapLevel(this MpBeatmap mpBeatmap, in BeatmapKey key, IPreviewMediaData previewMediaData) + { + var dict = new Dictionary<(BeatmapCharacteristicSO, BeatmapDifficulty), BeatmapBasicData>(); + dict[(key.beatmapCharacteristic, key.difficulty)] = new BeatmapBasicData( + 0, + 0, + EnvironmentName.Empty, + null, + 0, + 0, + 0, + new[] { mpBeatmap.LevelAuthorName }, + Array.Empty() + ); + + return new BeatmapLevel( + false, + mpBeatmap.LevelID, + mpBeatmap.SongName, + mpBeatmap.SongAuthorName, + mpBeatmap.SongSubName, + new[] { mpBeatmap.LevelAuthorName }, + Array.Empty(), + mpBeatmap.BeatsPerMinute, + -6.0f, + 0, + 0, + 0, + mpBeatmap.SongDuration, + PlayerSensitivityFlag.Safe, + previewMediaData, + dict + ); + } + } } diff --git a/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs b/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs index 1f3c0d8..a01bac1 100644 --- a/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs +++ b/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs @@ -7,6 +7,8 @@ using MultiplayerCore.UI; using MultiplayerCore.Objects; using Zenject; +using System.Diagnostics.Eventing.Reader; +using BGLib.Polyglot; namespace MultiplayerCore.Patchers { @@ -14,22 +16,85 @@ internal class GameServerPlayerTableCellPatcher : IAffinity { private MpPlayersDataModel _mpPlayersDataModel; - GameServerPlayerTableCellPatcher(MpPlayersDataModel mpPlayersDataModel) => _mpPlayersDataModel = mpPlayersDataModel; + GameServerPlayerTableCellPatcher(ILobbyPlayersDataModel playersDataModel) => _mpPlayersDataModel = playersDataModel as MpPlayersDataModel; + [AffinityPrefix] [AffinityPatch(typeof(GameServerPlayerTableCell), nameof(GameServerPlayerTableCell.SetData))] - void GameServerPlayerTableCell_SetData(GameServerPlayerTableCell ___instance, IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData) + bool GameServerPlayerTableCell_SetData(ref GameServerPlayerTableCell __instance, IConnectedPlayer connectedPlayer, ILobbyPlayerData? playerData, bool hasKickPermissions, bool allowSelection, Task? getLevelEntitlementTask) { - var beatmapKey = playerData.beatmapKey; - if (!beatmapKey.IsValid()) return; + __instance._playerNameText.text = connectedPlayer.userName; + __instance._localPlayerBackgroundImage.enabled = connectedPlayer.isMe; + if (!playerData.isReady && playerData.isActive && !playerData.isPartyOwner) + { + __instance._statusImageView.enabled = false; + } + else + { + __instance._statusImageView.enabled = true; - var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); - if (string.IsNullOrEmpty(levelHash)) return; + var statusView = __instance._statusImageView; + if (playerData.isPartyOwner) statusView.sprite = __instance._hostIcon; + else if (playerData.isActive) statusView.sprite = __instance._readyIcon; + else statusView.sprite = __instance._spectatingIcon; + } - var packet = _mpPlayersDataModel.PlayerPackets[connectedPlayer.userId]; - if (packet == null || packet.levelHash != levelHash) return; + var key = playerData.beatmapKey; + bool validKey = key.IsValid(); + bool displayLevelText = validKey; + if (validKey) + { + var level = __instance._beatmapLevelsModel.GetBeatmapLevel(key.levelId); + var levelHash = Utilities.HashForLevelID(key.levelId); + __instance._suggestedLevelText.text = level?.songName; + displayLevelText = level != null; - ___instance._suggestedLevelText.text = packet.songName; + if (level == null && _mpPlayersDataModel != null && !string.IsNullOrEmpty(levelHash)) // we didn't have the level, but we can attempt to get the packet + { + var packet = _mpPlayersDataModel.FindLevelPacket(levelHash); + __instance._suggestedLevelText.text = packet?.songName; + displayLevelText = packet != null; + } + + __instance._suggestedCharacteristicIcon.sprite = key.beatmapCharacteristic.icon; + __instance._suggestedDifficultyText.text = key.difficulty.ShortName(); + } + SetLevelFoundValues(__instance, displayLevelText); + bool anyModifiers = !(playerData?.gameplayModifiers?.IsWithoutModifiers() ?? true); + __instance._suggestedModifiersList.gameObject.SetActive(anyModifiers); + __instance._emptySuggestedModifiersText.gameObject.SetActive(!anyModifiers); + + if (anyModifiers) + { + var modifiers = __instance._gameplayModifiers.CreateModifierParamsList(playerData.gameplayModifiers); + __instance._emptySuggestedModifiersText.gameObject.SetActive(modifiers.Count == 0); + if (modifiers.Count > 0) + { + __instance._suggestedModifiersList.SetData(modifiers.Count, (int id, GameplayModifierInfoListItem listItem) => listItem.SetModifier(modifiers[id], false)); + } + } + + __instance._useModifiersButton.interactable = !connectedPlayer.isMe && anyModifiers && allowSelection; + __instance._kickPlayerButton.interactable = !connectedPlayer.isMe && hasKickPermissions && allowSelection; + __instance._mutePlayerButton.gameObject.SetActive(false); + if (getLevelEntitlementTask != null && !connectedPlayer.isMe) + { + __instance._useBeatmapButtonHoverHint.text = Localization.Get("LABEL_CANT_START_GAME_DO_NOT_OWN_SONG"); + __instance.SetBeatmapUseButtonEnabledAsync(getLevelEntitlementTask); + return false; + } + + __instance._useBeatmapButton.interactable = false; + __instance._useBeatmapButtonHoverHint.enabled = false; + + return false; } + void SetLevelFoundValues(GameServerPlayerTableCell __instance, bool displayLevelText) + { + __instance._suggestedLevelText.gameObject.SetActive(displayLevelText); + __instance._suggestedCharacteristicIcon.gameObject.SetActive(displayLevelText); + __instance._suggestedDifficultyText.gameObject.SetActive(displayLevelText); + __instance._emptySuggestedLevelText.gameObject.SetActive(!displayLevelText); + } } } From 43b03640e7b6076d39ee3e5e9af31ea3d674ce8c Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 1 May 2024 10:46:39 +0200 Subject: [PATCH 11/75] Add additional logging to entitlement check --- MultiplayerCore/Objects/MpEntitlementChecker.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Objects/MpEntitlementChecker.cs b/MultiplayerCore/Objects/MpEntitlementChecker.cs index 7a120ff..a4953ef 100644 --- a/MultiplayerCore/Objects/MpEntitlementChecker.cs +++ b/MultiplayerCore/Objects/MpEntitlementChecker.cs @@ -97,7 +97,7 @@ private void HandleSetIsEntitledToLevel(string userId, string levelId, Entitleme .Distinct().ToArray(); bool hasRequirements = requirements.All(x => string.IsNullOrEmpty(x) || SongCore.Collections.capabilities.Contains(x)); - return Task.FromResult(hasRequirements ? EntitlementsStatus.Ok : EntitlementsStatus.NotOwned); + return Task.FromResult(hasRequirements ? EntitlementsStatus.Ok : EntitlementsStatus.NotOwned); } return _beatsaver.BeatmapByHash(levelHash).ContinueWith(r => @@ -124,7 +124,9 @@ private void HandleSetIsEntitledToLevel(string userId, string levelId, Entitleme .ToArray()); // Damn this looks really cringe bool hasRequirements = requirements.All(x => string.IsNullOrEmpty(x) || SongCore.Collections.capabilities.Contains(x)); - return hasRequirements ? EntitlementsStatus.NotDownloaded : EntitlementsStatus.NotOwned; + if (hasRequirements) _logger.Debug($"Level hash {levelHash} found on BeatSaver!"); + else _logger.Warn($"Level hash {levelHash} requirements not fullfilled! {string.Join(", ", requirements)}"); + return hasRequirements ? EntitlementsStatus.NotDownloaded : EntitlementsStatus.NotOwned; }); } From 73524105fc3b5c32286576259f3aa2de7d68d396 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 1 May 2024 10:58:26 +0200 Subject: [PATCH 12/75] Fix level load --- MultiplayerCore/Objects/MpLevelLoader.cs | 121 +++++++++--------- .../MultiplayerLevelLoaderOverride.cs | 103 +++++++++++++++ 2 files changed, 165 insertions(+), 59 deletions(-) create mode 100644 MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index 4cd3df8..0f0695a 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -13,11 +13,11 @@ public class MpLevelLoader : MultiplayerLevelLoader, IProgress public ILevelGameplaySetupData? CurrentLoadingData => _gameplaySetupData; - private readonly IMultiplayerSessionManager _sessionManager; - private readonly MpLevelDownloader _levelDownloader; - private readonly MpEntitlementChecker _entitlementChecker; - private readonly IMenuRpcManager _rpcManager; - private readonly SiraLog _logger; + internal readonly IMultiplayerSessionManager _sessionManager; + internal readonly MpLevelDownloader _levelDownloader; + internal readonly MpEntitlementChecker _entitlementChecker; + internal readonly IMenuRpcManager _rpcManager; + internal readonly SiraLog _logger; internal MpLevelLoader( IMultiplayerSessionManager sessionManager, @@ -34,12 +34,11 @@ internal MpLevelLoader( } [UsedImplicitly] - public new void LoadLevel(ILevelGameplaySetupData gameplaySetupData, long initialStartTime) + public void LoadLevel_override(string levelId) { - var levelId = gameplaySetupData.beatmapKey.levelId; var levelHash = Utilities.HashForLevelID(levelId); - base.LoadLevel(gameplaySetupData, initialStartTime); + //base.LoadLevel(gameplaySetupData, initialStartTime); if (levelHash == null) { @@ -55,55 +54,54 @@ internal MpLevelLoader( _getBeatmapLevelResultTask = DownloadBeatmapLevelAsync(levelId, _getBeatmapCancellationTokenSource.Token); } - [UsedImplicitly] - public new void Tick() - { - if (_loaderState == MultiplayerBeatmapLoaderState.NotLoading) - { - // Loader: not doing anything - return; - } - - var levelId = _gameplaySetupData.beatmapKey.levelId; - - if (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) - { - // Loader: level is loaded locally, waiting for countdown to transition to level - // Modded behavior: wait until all players are ready before we transition - - if (_sessionManager.syncTime < _startTime) - return; - - // Ready check: player returned OK entitlement (load finished) OR already transitioned to gameplay - var allPlayersReady = _sessionManager.connectedPlayers.All(p => - _entitlementChecker.GetKnownEntitlement(p.userId, levelId) == EntitlementsStatus.Ok // level loaded - || p.HasState("in_gameplay") // already playing - || p.HasState("backgrounded") // not actively in game - || !p.HasState("wants_to_play_next_level") // doesn't want to play (spectator) - ); - - if (!allPlayersReady) - return; + //[UsedImplicitly] + //public void Tick_override() + //{ + // if (_loaderState == MultiplayerBeatmapLoaderState.NotLoading) + // { + // // Loader: not doing anything + // return; + // } + + // var levelId = _gameplaySetupData.beatmapKey.levelId; + + // if (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) + // { + // // Loader: level is loaded locally, waiting for countdown to transition to level + // // Modded behavior: wait until all players are ready before we transition + + // if (_sessionManager.syncTime < _startTime) + // return; + + // // Ready check: player returned OK entitlement (load finished) OR already transitioned to gameplay + // var allPlayersReady = _sessionManager.connectedPlayers.All(p => + // _entitlementChecker.GetKnownEntitlement(p.userId, levelId) == EntitlementsStatus.Ok // level loaded + // || p.HasState("in_gameplay") // already playing + // || p.HasState("backgrounded") // not actively in game + // || !p.HasState("wants_to_play_next_level") // doesn't want to play (spectator) + // ); + + // if (!allPlayersReady) + // return; - _logger.Debug($"All players finished loading"); - base.Tick(); // calling Tick() now will cause base level loader to transition to gameplay - return; - } + // _logger.Debug($"All players finished loading"); + // base.Tick(); // calling Tick() now will cause base level loader to transition to gameplay + // } - // Loader main: pending load - base.Tick(); + // // Loader main: pending load + // //base.Tick(); - var loadDidFinish = (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); - if (!loadDidFinish) - return; + // var loadDidFinish = (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); + // if (!loadDidFinish) + // return false; - _rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.Ok); - _logger.Debug($"Loaded level: {levelId}"); + // _rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.Ok); + // _logger.Debug($"Loaded level: {levelId}"); - UnloadLevelIfRequirementsNotMet(); - } + // UnloadLevelIfRequirementsNotMet(); + //} - private void UnloadLevelIfRequirementsNotMet() + internal void UnloadLevelIfRequirementsNotMet() { // Extra: load finished, check if there are extra requirements in place // If we fail requirements, unload the level @@ -158,13 +156,18 @@ private async Task DownloadBeatmapLevelAsync(string // Reload custom level set _logger.Debug("Reloading custom level collection..."); - await _beatmapLevelsModel.ReloadCustomLevelPackCollectionAsync(cancellationToken); - - // Load level data - var loadResult = await _beatmapLevelsModel.LoadBeatmapLevelDataAsync(levelId, cancellationToken); - if (loadResult.isError) - _logger.Error($"Custom level data could not be loaded after download: {levelId}"); - return loadResult; - } + //SongCore.Loader.Instance.RefreshSongs(false); + //await _beatmapLevelsModel.ReloadCustomLevelPackCollectionAsync(cancellationToken); + while (!SongCore.Loader.AreSongsLoaded) + { + await Task.Delay(25); + } + + // Load level data + var loadResult = await _beatmapLevelsModel.LoadBeatmapLevelDataAsync(levelId, cancellationToken); + if (loadResult.isError) + _logger.Error($"Custom level data could not be loaded after download: {levelId}"); + return loadResult; + } } } diff --git a/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs b/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs new file mode 100644 index 0000000..0d79b61 --- /dev/null +++ b/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs @@ -0,0 +1,103 @@ +using HarmonyLib; +using MultiplayerCore.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static IPA.Logging.Logger; +using static MultiplayerLevelLoader; + +namespace MultiplayerCore.Patches.OverridePatches +{ + [HarmonyPatch(typeof(MultiplayerLevelLoader))] + internal class MultiplayerLevelLoaderOverride + { + + [HarmonyPostfix] + [HarmonyPatch(nameof(MultiplayerLevelLoader.LoadLevel))] + private static void LoadLevel_override(MultiplayerLevelLoader __instance, ILevelGameplaySetupData gameplaySetupData, long initialStartTime) + { + Plugin.Logger.Debug("Called MultiplayerLevelLoader.LoadLevel Override Patch"); + ((MpLevelLoader)__instance).LoadLevel_override(gameplaySetupData.beatmapKey.levelId); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(MultiplayerLevelLoader.Tick))] + private static bool Tick_override_Pre(MultiplayerLevelLoader __instance, ValueTuple __state) + { + MpLevelLoader instance = (MpLevelLoader)__instance; + if (instance._loaderState == MultiplayerBeatmapLoaderState.NotLoading) + { + // Loader: not doing anything + return false; + } + + var levelId = instance._gameplaySetupData.beatmapKey.levelId; + __state = (false, levelId); + + if (instance._loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) + { + // Loader: level is loaded locally, waiting for countdown to transition to level + // Modded behavior: wait until all players are ready before we transition + + if (instance._sessionManager.syncTime < instance._startTime) + return false; + + // Ready check: player returned OK entitlement (load finished) OR already transitioned to gameplay + var allPlayersReady = instance._sessionManager.connectedPlayers.All(p => + instance._entitlementChecker.GetKnownEntitlement(p.userId, levelId) == EntitlementsStatus.Ok // level loaded + || p.HasState("in_gameplay") // already playing + || p.HasState("backgrounded") // not actively in game + || !p.HasState("wants_to_play_next_level") // doesn't want to play (spectator) + ); + + if (!allPlayersReady) + return false; + + instance._logger.Debug($"All players finished loading"); + //base.Tick(); // calling Tick() now will cause base level loader to transition to gameplay + //return true; + } + + // Loader main: pending load + __state.Item1 = true; + return true; + //base.Tick(); + //var loadDidFinish = (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); + //if (!loadDidFinish) + // return false; + + //_rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.Ok); + //_logger.Debug($"Loaded level: {levelId}"); + + //UnloadLevelIfRequirementsNotMet(); + } + + + [HarmonyPostfix] + [HarmonyPatch(nameof(MultiplayerLevelLoader.Tick))] + private static void Tick_override_Post(MultiplayerLevelLoader __instance, ValueTuple __state) + { + MpLevelLoader instance = (MpLevelLoader)__instance; + + if (!__state.Item1) + { + // Loader: not doing anything + return; + } + + // Loader main: pending load + var levelId = instance._gameplaySetupData.beatmapKey.levelId; + var loadDidFinish = (instance._loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); + if (!loadDidFinish) + return; + + instance._rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.Ok); + instance._logger.Debug($"Loaded level: {levelId}"); + + instance.UnloadLevelIfRequirementsNotMet(); + + } + } +} From d81622021103a7e7e9c7e21ce696ac479780f8e4 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 1 May 2024 10:59:41 +0200 Subject: [PATCH 13/75] Fix entitlement not sent --- MultiplayerCore/Objects/MpPlayersDataModel.cs | 4 ++-- .../PlayersDataModelOverride.cs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 MultiplayerCore/Patches/OverridePatches/PlayersDataModelOverride.cs diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index dcbc52e..bd5af8a 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -52,7 +52,7 @@ internal MpPlayersDataModel( public new void Dispose() => Deactivate(); - private new void SetPlayerBeatmapLevel(string userId, in BeatmapKey beatmapKey) + internal void SetPlayerBeatmapLevel_override(string userId, in BeatmapKey beatmapKey) { // Game: A player (can be the local player!) has selected / recommended a beatmap @@ -60,7 +60,7 @@ internal MpPlayersDataModel( // If local player: send extended beatmap info to other players _ = SendMpBeatmapPacket(beatmapKey); - base.SetPlayerBeatmapLevel(userId, in beatmapKey); + //base.SetPlayerBeatmapLevel(userId, in beatmapKey); } private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer player) diff --git a/MultiplayerCore/Patches/OverridePatches/PlayersDataModelOverride.cs b/MultiplayerCore/Patches/OverridePatches/PlayersDataModelOverride.cs new file mode 100644 index 0000000..35a3656 --- /dev/null +++ b/MultiplayerCore/Patches/OverridePatches/PlayersDataModelOverride.cs @@ -0,0 +1,23 @@ +using HarmonyLib; +using MultiplayerCore.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MultiplayerCore.Patches.OverridePatches +{ + [HarmonyPatch(typeof(LobbyPlayersDataModel))] + internal class PlayersDataModelOverride + { + + [HarmonyPrefix] + [HarmonyPatch(nameof(LobbyPlayersDataModel.SetPlayerBeatmapLevel))] + private static void SetPlayerBeatmapLevel_override(LobbyPlayersDataModel __instance, string userId, in BeatmapKey beatmapKey) + { + Plugin.Logger.Debug("Called LobbyPlayersDataModel.SetPlayerBeatmapLevel Override Patch"); + ((MpPlayersDataModel)__instance).SetPlayerBeatmapLevel_override(userId, beatmapKey); + } + } +} From a86f4951f69876e304602b8bd5c1a6ac92a5d5c7 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 1 May 2024 11:00:07 +0200 Subject: [PATCH 14/75] Fix logging transpiler --- MultiplayerCore/Patches/LoggingPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiplayerCore/Patches/LoggingPatch.cs b/MultiplayerCore/Patches/LoggingPatch.cs index ddb8672..7041d8d 100644 --- a/MultiplayerCore/Patches/LoggingPatch.cs +++ b/MultiplayerCore/Patches/LoggingPatch.cs @@ -25,7 +25,7 @@ private static IEnumerable PacketErrorLogger(IEnumerable Date: Wed, 1 May 2024 11:00:37 +0200 Subject: [PATCH 15/75] Temp fix exception when selecting other players suggestion --- MultiplayerCore/UI/MpRequirementsUI.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MultiplayerCore/UI/MpRequirementsUI.cs b/MultiplayerCore/UI/MpRequirementsUI.cs index 213cd1d..765a907 100644 --- a/MultiplayerCore/UI/MpRequirementsUI.cs +++ b/MultiplayerCore/UI/MpRequirementsUI.cs @@ -217,6 +217,7 @@ private void SetRequirementsFromPacket(MpBeatmapPacket packet) ClearCells(_levelInfoCells); var diff = packet.difficulty; + if (!packet.requirements.ContainsKey(diff)) { return; } foreach (var req in packet.requirements[diff]) { var cell = GetCellInfo(); From 09ac9508e741f20f4a26f88104d907fd22b9ec9c Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Mon, 6 May 2024 21:16:16 +0200 Subject: [PATCH 16/75] Fix patch not working properly Patreon release --- .../OverridePatches/MultiplayerLevelLoaderOverride.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs b/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs index 0d79b61..7e92304 100644 --- a/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs +++ b/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs @@ -24,7 +24,7 @@ private static void LoadLevel_override(MultiplayerLevelLoader __instance, ILevel [HarmonyPrefix] [HarmonyPatch(nameof(MultiplayerLevelLoader.Tick))] - private static bool Tick_override_Pre(MultiplayerLevelLoader __instance, ValueTuple __state) + private static bool Tick_override_Pre(MultiplayerLevelLoader __instance/*, ValueTuple __state*/) { MpLevelLoader instance = (MpLevelLoader)__instance; if (instance._loaderState == MultiplayerBeatmapLoaderState.NotLoading) @@ -34,7 +34,7 @@ private static bool Tick_override_Pre(MultiplayerLevelLoader __instance, ValueTu } var levelId = instance._gameplaySetupData.beatmapKey.levelId; - __state = (false, levelId); + //__state = (false, levelId); if (instance._loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) { @@ -61,7 +61,7 @@ private static bool Tick_override_Pre(MultiplayerLevelLoader __instance, ValueTu } // Loader main: pending load - __state.Item1 = true; + //__state.Item1 = true; return true; //base.Tick(); //var loadDidFinish = (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); @@ -77,11 +77,12 @@ private static bool Tick_override_Pre(MultiplayerLevelLoader __instance, ValueTu [HarmonyPostfix] [HarmonyPatch(nameof(MultiplayerLevelLoader.Tick))] - private static void Tick_override_Post(MultiplayerLevelLoader __instance, ValueTuple __state) + private static void Tick_override_Post(MultiplayerLevelLoader __instance/*, ValueTuple __state*/) { MpLevelLoader instance = (MpLevelLoader)__instance; - if (!__state.Item1) + //if (!__state.Item1) + if (instance._loaderState == MultiplayerBeatmapLoaderState.NotLoading) { // Loader: not doing anything return; From 978eb7f398c4bec1a4fd1b39b8f3e079f9d5c7be Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Thu, 9 May 2024 14:28:03 +0200 Subject: [PATCH 17/75] Fix entitlement spam during level load --- .../MultiplayerLevelLoaderOverride.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs b/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs index 7e92304..77c27c3 100644 --- a/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs +++ b/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs @@ -24,9 +24,10 @@ private static void LoadLevel_override(MultiplayerLevelLoader __instance, ILevel [HarmonyPrefix] [HarmonyPatch(nameof(MultiplayerLevelLoader.Tick))] - private static bool Tick_override_Pre(MultiplayerLevelLoader __instance/*, ValueTuple __state*/) + private static bool Tick_override_Pre(MultiplayerLevelLoader __instance, ref MultiplayerBeatmapLoaderState __state) { MpLevelLoader instance = (MpLevelLoader)__instance; + __state = instance._loaderState; if (instance._loaderState == MultiplayerBeatmapLoaderState.NotLoading) { // Loader: not doing anything @@ -77,28 +78,19 @@ private static bool Tick_override_Pre(MultiplayerLevelLoader __instance/*, Value [HarmonyPostfix] [HarmonyPatch(nameof(MultiplayerLevelLoader.Tick))] - private static void Tick_override_Post(MultiplayerLevelLoader __instance/*, ValueTuple __state*/) + private static void Tick_override_Post(MultiplayerLevelLoader __instance, MultiplayerBeatmapLoaderState __state) { MpLevelLoader instance = (MpLevelLoader)__instance; - //if (!__state.Item1) - if (instance._loaderState == MultiplayerBeatmapLoaderState.NotLoading) - { - // Loader: not doing anything - return; - } + bool loadJustFinished = __state == MultiplayerBeatmapLoaderState.LoadingBeatmap && instance._loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown; + if (!loadJustFinished) return; // Loader main: pending load var levelId = instance._gameplaySetupData.beatmapKey.levelId; - var loadDidFinish = (instance._loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); - if (!loadDidFinish) - return; - instance._rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.Ok); instance._logger.Debug($"Loaded level: {levelId}"); instance.UnloadLevelIfRequirementsNotMet(); - } } } From c5f50bbaf196e92a3b966327e5e2bf22bfca135c Mon Sep 17 00:00:00 2001 From: cubic Date: Tue, 14 May 2024 22:22:35 +0100 Subject: [PATCH 18/75] Fix node pose sync state and added score sync controls --- MultiplayerCore/Installers/MpAppInstaller.cs | 1 + .../MpNodePoseSyncStatePacket.cs | 8 +-- .../ScoreSyncState/MpScoreSyncStateManager.cs | 54 +++++++++++++++++++ .../ScoreSyncState/MpScoreSyncStatePacket.cs | 22 ++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs create mode 100644 MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs diff --git a/MultiplayerCore/Installers/MpAppInstaller.cs b/MultiplayerCore/Installers/MpAppInstaller.cs index 11f96d5..311d190 100644 --- a/MultiplayerCore/Installers/MpAppInstaller.cs +++ b/MultiplayerCore/Installers/MpAppInstaller.cs @@ -27,6 +27,7 @@ public override void InstallBindings() Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); + Container.BindInterfacesAndSelfTo().AsSingle(); Container.Bind().ToSelf().AsSingle(); Container.Bind().ToSelf().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); diff --git a/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStatePacket.cs b/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStatePacket.cs index beb3839..31744ee 100644 --- a/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStatePacket.cs +++ b/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStatePacket.cs @@ -5,8 +5,8 @@ namespace MultiplayerCore.NodePoseSyncState { internal class MpNodePoseSyncStatePacket : MpPacket { - public float deltaUpdateFrequency = 0.01f; - public float fullStateUpdateFrequency = 0.1f; + public long deltaUpdateFrequency = 10L; + public long fullStateUpdateFrequency = 100L; public override void Serialize(NetDataWriter writer) { writer.Put(deltaUpdateFrequency); @@ -15,8 +15,8 @@ public override void Serialize(NetDataWriter writer) public override void Deserialize(NetDataReader reader) { - deltaUpdateFrequency = reader.GetFloat(); - fullStateUpdateFrequency = reader.GetFloat(); + deltaUpdateFrequency = reader.GetVarLong(); + fullStateUpdateFrequency = reader.GetVarLong(); } } } diff --git a/MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs b/MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs new file mode 100644 index 0000000..b09fb56 --- /dev/null +++ b/MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs @@ -0,0 +1,54 @@ +using MultiplayerCore.Networking; +using System; +using Zenject; +using SiraUtil.Affinity; + + +namespace MultiplayerCore.NodePoseSyncState +{ + internal class MpScoreSyncStateManager : IInitializable, IDisposable, IAffinity + { + public float? DeltaUpdateFrequency { get; private set; } = null; + public float? FullStateUpdateFrequency { get; private set; } = null; + + private readonly MpPacketSerializer _packetSerializer; + MpScoreSyncStateManager(MpPacketSerializer packetSerializer) => _packetSerializer = packetSerializer; + + public void Initialize() => _packetSerializer.RegisterCallback(HandleUpdateFrequencyUpdated); + + public void Dispose() => _packetSerializer.UnregisterCallback(); + + private void HandleUpdateFrequencyUpdated(MpScoreSyncStatePacket data, IConnectedPlayer player) + { + if (player.isConnectionOwner) + { + DeltaUpdateFrequency = data.deltaUpdateFrequency; + FullStateUpdateFrequency = data.fullStateUpdateFrequency; + } + } + + [AffinityPrefix] + [AffinityPatch(typeof(ScoreSyncStateManager), "deltaUpdateFrequencyMs", AffinityMethodType.Getter)] + private bool GetDeltaUpdateFrequencyMs(ref long __result) + { + if (DeltaUpdateFrequency.HasValue) + { + __result = (long)(DeltaUpdateFrequency.Value * 1000); + return false; + } + return true; + } + + [AffinityPrefix] + [AffinityPatch(typeof(ScoreSyncStateManager), "fullStateUpdateFrequencyMs", AffinityMethodType.Getter)] + private bool GetFullStateUpdateFrequencyMs(ref long __result) + { + if (FullStateUpdateFrequency.HasValue) + { + __result = (long)(FullStateUpdateFrequency.Value * 1000); + return false; + } + return true; + } + } +} diff --git a/MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs b/MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs new file mode 100644 index 0000000..1e1320c --- /dev/null +++ b/MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs @@ -0,0 +1,22 @@ +using MultiplayerCore.Networking.Abstractions; +using LiteNetLib.Utils; + +namespace MultiplayerCore.NodePoseSyncState +{ + internal class MpScoreSyncStatePacket : MpPacket + { + public long deltaUpdateFrequency = 100L; + public long fullStateUpdateFrequency = 500L; + public override void Serialize(NetDataWriter writer) + { + writer.Put(deltaUpdateFrequency); + writer.Put(fullStateUpdateFrequency); + } + + public override void Deserialize(NetDataReader reader) + { + deltaUpdateFrequency = reader.GetVarLong(); + fullStateUpdateFrequency = reader.GetVarLong(); + } + } +} From 134768e6ec791245f066c6326c2fcf8e4a30eb6b Mon Sep 17 00:00:00 2001 From: cubic Date: Tue, 14 May 2024 22:26:43 +0100 Subject: [PATCH 19/75] Fix sync state values being multiplied --- .../NodePoseSyncState/MpNodePoseSyncStateManager.cs | 4 ++-- MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStateManager.cs b/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStateManager.cs index d930ab7..c04dc9e 100644 --- a/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStateManager.cs +++ b/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStateManager.cs @@ -33,7 +33,7 @@ private bool GetDeltaUpdateFrequencyMs(ref long __result) { if (DeltaUpdateFrequency.HasValue) { - __result = (long)(DeltaUpdateFrequency.Value * 1000); + __result = (long)(DeltaUpdateFrequency.Value); return false; } return true; @@ -45,7 +45,7 @@ private bool GetFullStateUpdateFrequencyMs(ref long __result) { if (FullStateUpdateFrequency.HasValue) { - __result = (long)(FullStateUpdateFrequency.Value * 1000); + __result = (long)(FullStateUpdateFrequency.Value); return false; } return true; diff --git a/MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs b/MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs index b09fb56..4a84bf3 100644 --- a/MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs +++ b/MultiplayerCore/ScoreSyncState/MpScoreSyncStateManager.cs @@ -33,7 +33,7 @@ private bool GetDeltaUpdateFrequencyMs(ref long __result) { if (DeltaUpdateFrequency.HasValue) { - __result = (long)(DeltaUpdateFrequency.Value * 1000); + __result = (long)(DeltaUpdateFrequency.Value); return false; } return true; @@ -45,7 +45,7 @@ private bool GetFullStateUpdateFrequencyMs(ref long __result) { if (FullStateUpdateFrequency.HasValue) { - __result = (long)(FullStateUpdateFrequency.Value * 1000); + __result = (long)(FullStateUpdateFrequency.Value); return false; } return true; From ae29445619f9a71a270c740b1ee7cbe9b37742fd Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 16 Jun 2024 19:39:50 +0200 Subject: [PATCH 20/75] Change from Setup to SetupData on LevelBar, BS 1.37 --- MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs index b2adee7..e1d65b5 100644 --- a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -42,8 +42,8 @@ public bool EditableBeatmapSelectionView_SetBeatmap(ref EditableBeatmapSelection __instance._levelBar.hide = false; var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); - __instance._levelBar.Setup(level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty); - return false; + __instance._levelBar.SetupData(level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic); + return false; } [AffinityPrefix] @@ -64,7 +64,7 @@ public bool BeatmapSelectionView_SetBeatmap(ref BeatmapSelectionView __instance, __instance._levelBar.hide = false; var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); - __instance._levelBar.Setup(level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty); + __instance._levelBar.SetupData(level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic); return false; } } @@ -87,6 +87,7 @@ public static BeatmapLevel MakeBeatmapLevel(this MpBeatmap mpBeatmap, in Beatmap ); return new BeatmapLevel( + 0, false, mpBeatmap.LevelID, mpBeatmap.SongName, From c1671a03f6f8508a879fe87e70451d376a45ddf0 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 16 Jun 2024 20:02:51 +0200 Subject: [PATCH 21/75] Change check for requirements in packet --- MultiplayerCore/UI/MpRequirementsUI.cs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/MultiplayerCore/UI/MpRequirementsUI.cs b/MultiplayerCore/UI/MpRequirementsUI.cs index 765a907..80bbe21 100644 --- a/MultiplayerCore/UI/MpRequirementsUI.cs +++ b/MultiplayerCore/UI/MpRequirementsUI.cs @@ -217,18 +217,20 @@ private void SetRequirementsFromPacket(MpBeatmapPacket packet) ClearCells(_levelInfoCells); var diff = packet.difficulty; - if (!packet.requirements.ContainsKey(diff)) { return; } - foreach (var req in packet.requirements[diff]) + if (packet.requirements.ContainsKey(diff)) { - var cell = GetCellInfo(); - bool installed = SongCore.Collections.capabilities.Contains(req); - cell.text = $"{req}"; - cell.subtext = installed ? "Requirement found" : "Requirement missing"; - cell.icon = installed ? HaveReqIcon : MissingReqIcon; - _levelInfoCells.Add(cell); - } - - foreach (var contributor in packet.contributors) + foreach (var req in packet.requirements[diff]) + { + var cell = GetCellInfo(); + bool installed = SongCore.Collections.capabilities.Contains(req); + cell.text = $"{req}"; + cell.subtext = installed ? "Requirement found" : "Requirement missing"; + cell.icon = installed ? HaveReqIcon : MissingReqIcon; + _levelInfoCells.Add(cell); + } + } + + foreach (var contributor in packet.contributors) { var cell = GetCellInfo(); cell.text = $"{contributor._name}"; From 0e299f33a529ba3dd37b1dd5a6d71667aabc3521 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Mon, 24 Jun 2024 21:50:35 +0200 Subject: [PATCH 22/75] Disable score validation when BeatmapBasicData notesCount is 0 --- ...MultiplayerLevelFinishedControllerPatch.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs diff --git a/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs b/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs new file mode 100644 index 0000000..969c627 --- /dev/null +++ b/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs @@ -0,0 +1,37 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MultiplayerCore.Patches +{ + [HarmonyPatch] + internal class MultiplayerLevelFinishedControllerPatch + { + [HarmonyPrefix] + [HarmonyPatch(typeof(MultiplayerLevelFinishedController), nameof(MultiplayerLevelFinishedController.HandleRpcLevelFinished))] + static bool HandleRpcLevelFinished(MultiplayerLevelFinishedController __instance, string userId, MultiplayerLevelCompletionResults results) + { + // Possibly get notesCount from BeatSaver or by parsing the beatmapdata ourselves + float maxMultipliedScore = ScoreModel.ComputeQuickInaccurateMaxMultipliedScoreForBeatmap(__instance._beatmapBasicData) * 1.21f; + if (!results.hasAnyResults) + Plugin.Logger.Info($"Score Received from user with id '{userId}' contains no results"); + // Skip score validation if notesCount is 0, since custom songs always have notesCount 0 in BeatmapBasicData + // TODO: Change this to only be a single if (__instance._beatmapBasicData.notesCount <= 0 && results.hasAnyResults) if check + else if (__instance._beatmapBasicData.notesCount <= 0) + { + Plugin.Logger.Info($"BeatmapData noteCount is 0, skipping validation"); + __instance._otherPlayersCompletionResults[userId] = results; + return false; + } + else if (results.levelCompletionResults.modifiedScore <= maxMultipliedScore && results.levelCompletionResults.modifiedScore >= 0) + Plugin.Logger.Info($"Score Received from user with id '{userId}' contains results and is valid, modifiedScore: '{results.levelCompletionResults.modifiedScore}'"); + else + Plugin.Logger.Info($"Score Received from user with id '{userId}' failed validation, maxMultipliedScore: '{maxMultipliedScore}', modifiedScore: '{results.levelCompletionResults.modifiedScore}'"); + return true; + } + } + +} From 071eac3c43bbf7a9f6c217f4452a43b0b7f30b21 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Mon, 24 Jun 2024 21:52:45 +0200 Subject: [PATCH 23/75] Cleanup code --- .../Providers/MpBeatmapLevelProvider.cs | 48 +++++++++---------- .../MultiplayerLevelLoaderOverride.cs | 13 ----- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs index d0f4bed..72fef57 100644 --- a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs +++ b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs @@ -21,21 +21,21 @@ internal MpBeatmapLevelProvider( _beatsaver = beatsaver.Value; } - /// - /// Gets an for the specified level hash. - /// - /// The hash of the level to get - /// An with a matching level hash - public async Task GetBeatmap(string levelHash) + /// + /// Gets an for the specified level hash. + /// + /// The hash of the level to get + /// An with a matching level hash + public async Task GetBeatmap(string levelHash) => GetBeatmapFromLocalBeatmaps(levelHash) ?? await GetBeatmapFromBeatSaver(levelHash); - /// - /// Gets an for the specified level hash from local, already downloaded beatmaps. - /// - /// The hash of the level to get - /// An with a matching level hash, or null if none was found. - public MpBeatmap? GetBeatmapFromLocalBeatmaps(string levelHash) + /// + /// Gets an for the specified level hash from local, already downloaded beatmaps. + /// + /// The hash of the level to get + /// An with a matching level hash, or null if none was found. + public MpBeatmap? GetBeatmapFromLocalBeatmaps(string levelHash) { var localBeatmapLevel = SongCore.Loader.GetLevelByHash(levelHash); if (localBeatmapLevel == null) @@ -44,12 +44,12 @@ internal MpBeatmapLevelProvider( return new LocalBeatmapLevel(levelHash, localBeatmapLevel); } - /// - /// Gets an for the specified level hash from BeatSaver. - /// - /// The hash of the level to get - /// An with a matching level hash, or null if none was found. - public async Task GetBeatmapFromBeatSaver(string levelHash) + /// + /// Gets an for the specified level hash from BeatSaver. + /// + /// The hash of the level to get + /// An with a matching level hash, or null if none was found. + public async Task GetBeatmapFromBeatSaver(string levelHash) { if (_hashToBeatsaverMaps.TryGetValue(levelHash, out var map)) return map; var beatmap = await _beatsaver.BeatmapByHash(levelHash); @@ -65,12 +65,12 @@ internal MpBeatmapLevelProvider( public BeatSaverPreviewMediaData MakeBeatSaverPreviewMediaData(string levelHash) => new BeatSaverPreviewMediaData(_beatsaver, levelHash); - /// - /// Gets an from the information in the provided packet. - /// - /// The packet to get preview data from - /// An with a cover from BeatSaver. - public MpBeatmap GetBeatmapFromPacket(MpBeatmapPacket packet) + /// + /// Gets an from the information in the provided packet. + /// + /// The packet to get preview data from + /// An with a cover from BeatSaver. + public MpBeatmap GetBeatmapFromPacket(MpBeatmapPacket packet) { if (_hashToNetworkMaps.TryGetValue(packet.levelHash, out var map)) return map; map = new NetworkBeatmapLevel(packet); diff --git a/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs b/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs index 77c27c3..9d745b5 100644 --- a/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs +++ b/MultiplayerCore/Patches/OverridePatches/MultiplayerLevelLoaderOverride.cs @@ -35,7 +35,6 @@ private static bool Tick_override_Pre(MultiplayerLevelLoader __instance, ref Mul } var levelId = instance._gameplaySetupData.beatmapKey.levelId; - //__state = (false, levelId); if (instance._loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) { @@ -57,22 +56,10 @@ private static bool Tick_override_Pre(MultiplayerLevelLoader __instance, ref Mul return false; instance._logger.Debug($"All players finished loading"); - //base.Tick(); // calling Tick() now will cause base level loader to transition to gameplay - //return true; } // Loader main: pending load - //__state.Item1 = true; return true; - //base.Tick(); - //var loadDidFinish = (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); - //if (!loadDidFinish) - // return false; - - //_rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.Ok); - //_logger.Debug($"Loaded level: {levelId}"); - - //UnloadLevelIfRequirementsNotMet(); } From 84799b1b0e979278db05bbc8b33bb5d24efdc1b9 Mon Sep 17 00:00:00 2001 From: rcelyte Date: Wed, 26 Jun 2024 20:27:47 +0000 Subject: [PATCH 24/75] Fix MpBeatmapPacket ordering (#51) --- MultiplayerCore/Objects/MpPlayersDataModel.cs | 17 ++++++++--------- .../OverridePatches/PlayersDataModelOverride.cs | 8 ++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index bd5af8a..420e1f5 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -52,15 +52,14 @@ internal MpPlayersDataModel( public new void Dispose() => Deactivate(); - internal void SetPlayerBeatmapLevel_override(string userId, in BeatmapKey beatmapKey) + internal void SetLocalPlayerBeatmapLevel_override(in BeatmapKey beatmapKey) { - // Game: A player (can be the local player!) has selected / recommended a beatmap + // Game: The local player has selected / recommended a beatmap - if (userId == _multiplayerSessionManager.localPlayer.userId) - // If local player: send extended beatmap info to other players - _ = SendMpBeatmapPacket(beatmapKey); + // send extended beatmap info to other players + SendMpBeatmapPacket(beatmapKey); - //base.SetPlayerBeatmapLevel(userId, in beatmapKey); + //base.SetLocalPlayerBeatmapLevel(userId, in beatmapKey); } private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer player) @@ -81,7 +80,7 @@ private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer // RPC: The server / another player has asked us to send our recommended beatmap var selectedBeatmapKey = _playersData[localUserId].beatmapKey; - _ = SendMpBeatmapPacket(selectedBeatmapKey); + SendMpBeatmapPacket(selectedBeatmapKey); base.HandleMenuRpcManagerGetRecommendedBeatmap(userId); } @@ -96,7 +95,7 @@ private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer base.HandleMenuRpcManagerRecommendBeatmap(userId, beatmapKeySerializable); } - private async Task SendMpBeatmapPacket(BeatmapKey beatmapKey) + private void SendMpBeatmapPacket(BeatmapKey beatmapKey) { var levelId = beatmapKey.levelId; _logger.Debug($"Sending beatmap packet for level {levelId}"); @@ -108,7 +107,7 @@ private async Task SendMpBeatmapPacket(BeatmapKey beatmapKey) return; } - var levelData = await _beatmapLevelProvider.GetBeatmap(levelHash); + var levelData = _beatmapLevelProvider.GetBeatmapFromLocalBeatmaps(levelHash); if (levelData == null) { _logger.Debug("Could not get level data for beatmap, returning!"); diff --git a/MultiplayerCore/Patches/OverridePatches/PlayersDataModelOverride.cs b/MultiplayerCore/Patches/OverridePatches/PlayersDataModelOverride.cs index 35a3656..499beac 100644 --- a/MultiplayerCore/Patches/OverridePatches/PlayersDataModelOverride.cs +++ b/MultiplayerCore/Patches/OverridePatches/PlayersDataModelOverride.cs @@ -13,11 +13,11 @@ internal class PlayersDataModelOverride { [HarmonyPrefix] - [HarmonyPatch(nameof(LobbyPlayersDataModel.SetPlayerBeatmapLevel))] - private static void SetPlayerBeatmapLevel_override(LobbyPlayersDataModel __instance, string userId, in BeatmapKey beatmapKey) + [HarmonyPatch(nameof(LobbyPlayersDataModel.SetLocalPlayerBeatmapLevel))] + private static void SetLocalPlayerBeatmapLevel_override(LobbyPlayersDataModel __instance, in BeatmapKey beatmapKey) { - Plugin.Logger.Debug("Called LobbyPlayersDataModel.SetPlayerBeatmapLevel Override Patch"); - ((MpPlayersDataModel)__instance).SetPlayerBeatmapLevel_override(userId, beatmapKey); + Plugin.Logger.Debug("Called LobbyPlayersDataModel.SetLocalPlayerBeatmapLevel Override Patch"); + ((MpPlayersDataModel)__instance).SetLocalPlayerBeatmapLevel_override(beatmapKey); } } } From c5146a64dce29c4182565c2679b8ff7dfd49e92a Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 26 Jun 2024 23:27:20 +0200 Subject: [PATCH 25/75] Add compat for 1.35, code cleanup --- .../Patchers/BeatmapSelectionViewPatcher.cs | 172 +++++++++++++----- 1 file changed, 131 insertions(+), 41 deletions(-) diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs index e1d65b5..e8322e3 100644 --- a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -1,15 +1,14 @@ -using MultiplayerCore.Beatmaps; +using HarmonyLib; using MultiplayerCore.Beatmaps.Abstractions; -using MultiplayerCore.Beatmaps.Packets; using MultiplayerCore.Beatmaps.Providers; using MultiplayerCore.Objects; +using SiraUtil.Affinity; using System; using System.Collections.Generic; -using SiraUtil.Affinity; namespace MultiplayerCore.Patchers { - internal class BeatmapSelectionViewPatcher : IAffinity + internal class BeatmapSelectionViewPatcher : IAffinity { private MpPlayersDataModel _mpPlayersDataModel; private MpBeatmapLevelProvider _mpBeatmapLevelProvider; @@ -32,7 +31,7 @@ public bool EditableBeatmapSelectionView_SetBeatmap(ref EditableBeatmapSelection if (_beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId) != null) return true; var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); - if (String.IsNullOrWhiteSpace(levelHash)) return true; + if (string.IsNullOrWhiteSpace(levelHash)) return true; var packet = _mpPlayersDataModel.FindLevelPacket(levelHash!); if (packet == null) return true; @@ -42,7 +41,19 @@ public bool EditableBeatmapSelectionView_SetBeatmap(ref EditableBeatmapSelection __instance._levelBar.hide = false; var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); - __instance._levelBar.SetupData(level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic); + + // For 1.35 + var mInfo = __instance._levelBar.GetType().GetMethod("Setup", new Type[] { level.GetType(), beatmapKey.beatmapCharacteristic.GetType(), beatmapKey.difficulty.GetType() }); + if (mInfo != null) + mInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); + else + { + // For 1.37 + mInfo = __instance._levelBar.GetType().GetMethod("SetupData", new Type[] { level.GetType(), beatmapKey.difficulty.GetType(), beatmapKey.beatmapCharacteristic.GetType() }); + if (mInfo != null) + mInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); + else Plugin.Logger.Critical("Can't find a fitting LevelBar Method, is your game version supported?"); + } return false; } @@ -55,7 +66,7 @@ public bool BeatmapSelectionView_SetBeatmap(ref BeatmapSelectionView __instance, if (_beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId) != null) return true; var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); - if (String.IsNullOrWhiteSpace(levelHash)) return true; + if (string.IsNullOrWhiteSpace(levelHash)) return true; var packet = _mpPlayersDataModel.FindLevelPacket(levelHash!); if (packet == null) return true; @@ -64,8 +75,19 @@ public bool BeatmapSelectionView_SetBeatmap(ref BeatmapSelectionView __instance, __instance._levelBar.hide = false; var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); - __instance._levelBar.SetupData(level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic); - return false; + + + var mInfo = __instance._levelBar.GetType().GetMethod("Setup", new Type[] { level.GetType(), beatmapKey.beatmapCharacteristic.GetType(), beatmapKey.difficulty.GetType() }); + if (mInfo != null) + mInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); + else + { + mInfo = __instance._levelBar.GetType().GetMethod("SetupData", new Type[] { level.GetType(), beatmapKey.difficulty.GetType(), beatmapKey.beatmapCharacteristic.GetType() }); + if (mInfo != null) + mInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); + else Plugin.Logger.Critical("Can't find a fitting LevelBar Method, is your game version supported?"); + } + return false; } } @@ -73,38 +95,106 @@ static internal class PacketExt { public static BeatmapLevel MakeBeatmapLevel(this MpBeatmap mpBeatmap, in BeatmapKey key, IPreviewMediaData previewMediaData) { - var dict = new Dictionary<(BeatmapCharacteristicSO, BeatmapDifficulty), BeatmapBasicData>(); - dict[(key.beatmapCharacteristic, key.difficulty)] = new BeatmapBasicData( - 0, - 0, - EnvironmentName.Empty, - null, - 0, - 0, - 0, - new[] { mpBeatmap.LevelAuthorName }, - Array.Empty() - ); - - return new BeatmapLevel( - 0, - false, - mpBeatmap.LevelID, - mpBeatmap.SongName, - mpBeatmap.SongAuthorName, - mpBeatmap.SongSubName, - new[] { mpBeatmap.LevelAuthorName }, - Array.Empty(), - mpBeatmap.BeatsPerMinute, - -6.0f, - 0, - 0, - 0, - mpBeatmap.SongDuration, - PlayerSensitivityFlag.Safe, - previewMediaData, - dict - ); + var dict = new Dictionary<(BeatmapCharacteristicSO, BeatmapDifficulty), BeatmapBasicData> + { + [(key.beatmapCharacteristic, key.difficulty)] = new BeatmapBasicData( + 0, + 0, + EnvironmentName.Empty, + null, + 0, + 0, + 0, + new[] { mpBeatmap.LevelAuthorName }, + Array.Empty() + ) + }; + + // For 1.35 + var conInfo = AccessTools.Constructor(typeof(BeatmapLevel), new Type[] + { + typeof(bool), + typeof(string), + typeof(string), + typeof(string), + typeof(string), + typeof(string[]), + typeof(string[]), + typeof(float), + typeof(float), + typeof(float), + typeof(float), + typeof(float), + typeof(float), + typeof(PlayerSensitivityFlag), + typeof(IPreviewMediaData), + typeof(IReadOnlyDictionary<(BeatmapCharacteristicSO, BeatmapDifficulty), BeatmapBasicData>) + }); + if (conInfo != null) + { + return (BeatmapLevel)conInfo.Invoke(new object[] + { + false, + mpBeatmap.LevelID, + mpBeatmap.SongName, + mpBeatmap.SongAuthorName, + mpBeatmap.SongSubName, + new[] { mpBeatmap.LevelAuthorName }, + Array.Empty(), + mpBeatmap.BeatsPerMinute, + -6.0f, + 0, + 0, + 0, + mpBeatmap.SongDuration, + PlayerSensitivityFlag.Safe, + previewMediaData, + dict + }); + } + // For 1.37 + conInfo = AccessTools.Constructor(typeof(BeatmapLevel), new Type[] + { + typeof(int), + typeof(bool), + typeof(string), + typeof(string), + typeof(string), + typeof(string), + typeof(string[]), + typeof(string[]), + typeof(float), + typeof(float), + typeof(float), + typeof(float), + typeof(float), + typeof(float), + typeof(PlayerSensitivityFlag), + typeof(IPreviewMediaData), + typeof(IReadOnlyDictionary<(BeatmapCharacteristicSO, BeatmapDifficulty), BeatmapBasicData>) + }); + if (conInfo != null) + return (BeatmapLevel)conInfo.Invoke(new object[] + { + 0, + false, + mpBeatmap.LevelID, + mpBeatmap.SongName, + mpBeatmap.SongAuthorName, + mpBeatmap.SongSubName, + new[] { mpBeatmap.LevelAuthorName }, + Array.Empty(), + mpBeatmap.BeatsPerMinute, + -6.0f, + 0, + 0, + 0, + mpBeatmap.SongDuration, + PlayerSensitivityFlag.Safe, + previewMediaData, + dict + }); + throw new NotSupportedException("Game Version not supported"); } } } From 7c4de8940071531f01722e01ac4ce4665e9b1130 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Fri, 12 Jul 2024 22:00:53 +0200 Subject: [PATCH 26/75] Fix MpNodePoseSyncStatePacket should use VarLong --- .../NodePoseSyncState/MpNodePoseSyncStatePacket.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStatePacket.cs b/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStatePacket.cs index 31744ee..8360389 100644 --- a/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStatePacket.cs +++ b/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStatePacket.cs @@ -9,8 +9,8 @@ internal class MpNodePoseSyncStatePacket : MpPacket public long fullStateUpdateFrequency = 100L; public override void Serialize(NetDataWriter writer) { - writer.Put(deltaUpdateFrequency); - writer.Put(fullStateUpdateFrequency); + writer.PutVarLong(deltaUpdateFrequency); + writer.PutVarLong(fullStateUpdateFrequency); } public override void Deserialize(NetDataReader reader) From 9b34e3ff03eee7f2b471da14de317147dcff62e4 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Fri, 12 Jul 2024 22:03:16 +0200 Subject: [PATCH 27/75] Code cleanup/optimization --- MultiplayerCore/Objects/MpLevelLoader.cs | 55 +---------------- .../Patchers/BeatmapSelectionViewPatcher.cs | 60 +++++++++++-------- 2 files changed, 36 insertions(+), 79 deletions(-) diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index 0f0695a..a45cd22 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -38,8 +38,6 @@ public void LoadLevel_override(string levelId) { var levelHash = Utilities.HashForLevelID(levelId); - //base.LoadLevel(gameplaySetupData, initialStartTime); - if (levelHash == null) { _logger.Debug($"Ignoring level (not a custom level hash): {levelId}"); @@ -54,53 +52,6 @@ public void LoadLevel_override(string levelId) _getBeatmapLevelResultTask = DownloadBeatmapLevelAsync(levelId, _getBeatmapCancellationTokenSource.Token); } - //[UsedImplicitly] - //public void Tick_override() - //{ - // if (_loaderState == MultiplayerBeatmapLoaderState.NotLoading) - // { - // // Loader: not doing anything - // return; - // } - - // var levelId = _gameplaySetupData.beatmapKey.levelId; - - // if (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown) - // { - // // Loader: level is loaded locally, waiting for countdown to transition to level - // // Modded behavior: wait until all players are ready before we transition - - // if (_sessionManager.syncTime < _startTime) - // return; - - // // Ready check: player returned OK entitlement (load finished) OR already transitioned to gameplay - // var allPlayersReady = _sessionManager.connectedPlayers.All(p => - // _entitlementChecker.GetKnownEntitlement(p.userId, levelId) == EntitlementsStatus.Ok // level loaded - // || p.HasState("in_gameplay") // already playing - // || p.HasState("backgrounded") // not actively in game - // || !p.HasState("wants_to_play_next_level") // doesn't want to play (spectator) - // ); - - // if (!allPlayersReady) - // return; - - // _logger.Debug($"All players finished loading"); - // base.Tick(); // calling Tick() now will cause base level loader to transition to gameplay - // } - - // // Loader main: pending load - // //base.Tick(); - - // var loadDidFinish = (_loaderState == MultiplayerBeatmapLoaderState.WaitingForCountdown); - // if (!loadDidFinish) - // return false; - - // _rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.Ok); - // _logger.Debug($"Loaded level: {levelId}"); - - // UnloadLevelIfRequirementsNotMet(); - //} - internal void UnloadLevelIfRequirementsNotMet() { // Extra: load finished, check if there are extra requirements in place @@ -156,15 +107,13 @@ private async Task DownloadBeatmapLevelAsync(string // Reload custom level set _logger.Debug("Reloading custom level collection..."); - //SongCore.Loader.Instance.RefreshSongs(false); - //await _beatmapLevelsModel.ReloadCustomLevelPackCollectionAsync(cancellationToken); while (!SongCore.Loader.AreSongsLoaded) { await Task.Delay(25); } - + // Load level data - var loadResult = await _beatmapLevelsModel.LoadBeatmapLevelDataAsync(levelId, cancellationToken); + var loadResult = await _beatmapLevelsModel.LoadBeatmapLevelDataAsync(levelId, BeatmapLevelDataVersion.Original, cancellationToken); if (loadResult.isError) _logger.Error($"Custom level data could not be loaded after download: {levelId}"); return loadResult; diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs index e8322e3..74cd0c4 100644 --- a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -5,6 +5,7 @@ using SiraUtil.Affinity; using System; using System.Collections.Generic; +using System.Reflection; namespace MultiplayerCore.Patchers { @@ -14,15 +15,27 @@ internal class BeatmapSelectionViewPatcher : IAffinity private MpBeatmapLevelProvider _mpBeatmapLevelProvider; private BeatmapLevelsModel _beatmapLevelsModel; + private static MethodInfo _lbarInfo; + private static bool _newlbarInfo; + BeatmapSelectionViewPatcher(ILobbyPlayersDataModel playersDataModel, MpBeatmapLevelProvider mpBeatmapLevelProvider, BeatmapLevelsModel beatmapLevelsModel) { _mpPlayersDataModel = playersDataModel as MpPlayersDataModel; _mpBeatmapLevelProvider = mpBeatmapLevelProvider; _beatmapLevelsModel = beatmapLevelsModel; - } - [AffinityPrefix] + _lbarInfo = AccessTools.Method(typeof(LevelBar), "SetupData", + new Type[] { typeof(BeatmapLevel), typeof(BeatmapDifficulty), typeof(BeatmapCharacteristicSO) }); + if (_lbarInfo != null) _newlbarInfo = true; + else _lbarInfo = AccessTools.Method(typeof(LevelBar), "Setup", new Type[] { typeof(BeatmapLevel), typeof(BeatmapCharacteristicSO), typeof(BeatmapDifficulty) }); + if (_lbarInfo == null) + { + Plugin.Logger.Critical("Can't find a fitting LevelBar Method, is your game version supported?"); + } + } + + [AffinityPrefix] [AffinityPatch(typeof(EditableBeatmapSelectionView), nameof(EditableBeatmapSelectionView.SetBeatmap))] public bool EditableBeatmapSelectionView_SetBeatmap(ref EditableBeatmapSelectionView __instance, in BeatmapKey beatmapKey) { @@ -42,18 +55,15 @@ public bool EditableBeatmapSelectionView_SetBeatmap(ref EditableBeatmapSelection var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); - // For 1.35 - var mInfo = __instance._levelBar.GetType().GetMethod("Setup", new Type[] { level.GetType(), beatmapKey.beatmapCharacteristic.GetType(), beatmapKey.difficulty.GetType() }); - if (mInfo != null) - mInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); - else - { - // For 1.37 - mInfo = __instance._levelBar.GetType().GetMethod("SetupData", new Type[] { level.GetType(), beatmapKey.difficulty.GetType(), beatmapKey.beatmapCharacteristic.GetType() }); - if (mInfo != null) - mInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); - else Plugin.Logger.Critical("Can't find a fitting LevelBar Method, is your game version supported?"); - } + Plugin.Logger.Debug($"Calling Setup/SetupData with level type: {level.GetType().Name}, beatmapCharacteristic type: {beatmapKey.beatmapCharacteristic.GetType().Name}, difficulty type: {beatmapKey.difficulty.GetType().Name} "); + if (_newlbarInfo) + { + _lbarInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); + } + else + { + _lbarInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); + } return false; } @@ -76,22 +86,20 @@ public bool BeatmapSelectionView_SetBeatmap(ref BeatmapSelectionView __instance, var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); - - var mInfo = __instance._levelBar.GetType().GetMethod("Setup", new Type[] { level.GetType(), beatmapKey.beatmapCharacteristic.GetType(), beatmapKey.difficulty.GetType() }); - if (mInfo != null) - mInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); - else - { - mInfo = __instance._levelBar.GetType().GetMethod("SetupData", new Type[] { level.GetType(), beatmapKey.difficulty.GetType(), beatmapKey.beatmapCharacteristic.GetType() }); - if (mInfo != null) - mInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); - else Plugin.Logger.Critical("Can't find a fitting LevelBar Method, is your game version supported?"); - } + Plugin.Logger.Debug($"Calling Setup/SetupData with level type: {level.GetType().Name}, beatmapCharacteristic type: {beatmapKey.beatmapCharacteristic.GetType().Name}, difficulty type: {beatmapKey.difficulty.GetType().Name} "); + if (_newlbarInfo) + { + _lbarInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); + } + else + { + _lbarInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); + } return false; } } - static internal class PacketExt + internal static class PacketExt { public static BeatmapLevel MakeBeatmapLevel(this MpBeatmap mpBeatmap, in BeatmapKey key, IPreviewMediaData previewMediaData) { From 9ac17c596b2cfa1435ecbaa91b8bd51db82ddfbb Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Fri, 12 Jul 2024 22:05:43 +0200 Subject: [PATCH 28/75] No score validation for custom levels --- .../Patches/MultiplayerLevelFinishedControllerPatch.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs b/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs index 969c627..b7ae202 100644 --- a/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs +++ b/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs @@ -15,21 +15,14 @@ internal class MultiplayerLevelFinishedControllerPatch static bool HandleRpcLevelFinished(MultiplayerLevelFinishedController __instance, string userId, MultiplayerLevelCompletionResults results) { // Possibly get notesCount from BeatSaver or by parsing the beatmapdata ourselves - float maxMultipliedScore = ScoreModel.ComputeQuickInaccurateMaxMultipliedScoreForBeatmap(__instance._beatmapBasicData) * 1.21f; - if (!results.hasAnyResults) - Plugin.Logger.Info($"Score Received from user with id '{userId}' contains no results"); // Skip score validation if notesCount is 0, since custom songs always have notesCount 0 in BeatmapBasicData // TODO: Change this to only be a single if (__instance._beatmapBasicData.notesCount <= 0 && results.hasAnyResults) if check - else if (__instance._beatmapBasicData.notesCount <= 0) + if (__instance._beatmapBasicData.notesCount <= 0 && results.hasAnyResults) { Plugin.Logger.Info($"BeatmapData noteCount is 0, skipping validation"); __instance._otherPlayersCompletionResults[userId] = results; return false; } - else if (results.levelCompletionResults.modifiedScore <= maxMultipliedScore && results.levelCompletionResults.modifiedScore >= 0) - Plugin.Logger.Info($"Score Received from user with id '{userId}' contains results and is valid, modifiedScore: '{results.levelCompletionResults.modifiedScore}'"); - else - Plugin.Logger.Info($"Score Received from user with id '{userId}' failed validation, maxMultipliedScore: '{maxMultipliedScore}', modifiedScore: '{results.levelCompletionResults.modifiedScore}'"); return true; } } From 3c4ef8169f91b75c71f77160b543457270deea45 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Fri, 12 Jul 2024 22:07:55 +0200 Subject: [PATCH 29/75] Add POC UI for per player difficulty and placeholder VCs --- MultiplayerCore/Installers/MpMenuInstaller.cs | 14 +- .../Players/Packets/GetPerPlayer.cs | 17 +++ MultiplayerCore/Players/Packets/PerPlayer.cs | 28 ++++ .../UI/LobbySettingsViewController.bsml | 6 + .../UI/LobbySettingsViewController.cs | 64 ++++++++ MultiplayerCore/UI/LobbySetupPanel.bsml | 13 ++ MultiplayerCore/UI/LobbySetupPanel.cs | 139 ++++++++++++++++++ MultiplayerCore/UI/MpCoreGameplaySetup.bsml | 6 + MultiplayerCore/UI/MpCoreGameplaySetup.cs | 104 +++++++++++++ .../UI/MpCoreSetupFlowCoordinator.cs | 62 ++++++++ 10 files changed, 451 insertions(+), 2 deletions(-) create mode 100644 MultiplayerCore/Players/Packets/GetPerPlayer.cs create mode 100644 MultiplayerCore/Players/Packets/PerPlayer.cs create mode 100644 MultiplayerCore/UI/LobbySettingsViewController.bsml create mode 100644 MultiplayerCore/UI/LobbySettingsViewController.cs create mode 100644 MultiplayerCore/UI/LobbySetupPanel.bsml create mode 100644 MultiplayerCore/UI/LobbySetupPanel.cs create mode 100644 MultiplayerCore/UI/MpCoreGameplaySetup.bsml create mode 100644 MultiplayerCore/UI/MpCoreGameplaySetup.cs create mode 100644 MultiplayerCore/UI/MpCoreSetupFlowCoordinator.cs diff --git a/MultiplayerCore/Installers/MpMenuInstaller.cs b/MultiplayerCore/Installers/MpMenuInstaller.cs index 923800c..7a5479b 100644 --- a/MultiplayerCore/Installers/MpMenuInstaller.cs +++ b/MultiplayerCore/Installers/MpMenuInstaller.cs @@ -4,7 +4,7 @@ namespace MultiplayerCore.Installers { - internal class MpMenuInstaller : Installer + internal class MpMenuInstaller : MonoInstaller { public override void InstallBindings() { @@ -17,5 +17,15 @@ public override void InstallBindings() // Inject sira stuff that didn't get injected on appinit Container.Inject(Container.Resolve()); } - } + + public override void Start() + { + Plugin.Logger?.Info("Installing Interface"); + + LobbySetupViewController lobbySetupViewController = Container.Resolve(); + Container.InstantiateComponent(lobbySetupViewController.gameObject); + + Plugin.Logger?.Info("Installed Interface"); + } + } } diff --git a/MultiplayerCore/Players/Packets/GetPerPlayer.cs b/MultiplayerCore/Players/Packets/GetPerPlayer.cs new file mode 100644 index 0000000..b158e14 --- /dev/null +++ b/MultiplayerCore/Players/Packets/GetPerPlayer.cs @@ -0,0 +1,17 @@ +using LiteNetLib.Utils; +using MultiplayerCore.Networking.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MultiplayerCore.Players.Packets +{ + internal class GetPerPlayer : MpPacket + { + public override void Deserialize(NetDataReader reader) { } + + public override void Serialize(NetDataWriter writer) { } + } +} diff --git a/MultiplayerCore/Players/Packets/PerPlayer.cs b/MultiplayerCore/Players/Packets/PerPlayer.cs new file mode 100644 index 0000000..11649bc --- /dev/null +++ b/MultiplayerCore/Players/Packets/PerPlayer.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using LiteNetLib.Utils; +using MultiplayerCore.Networking.Abstractions; + +namespace MultiplayerCore.Players.Packets +{ + internal class PerPlayer : MpPacket + { + public bool PPDEnabled; + public bool PPMEnabled; + + public override void Deserialize(NetDataReader reader) + { + PPDEnabled = reader.GetBool(); + PPMEnabled = reader.GetBool(); + } + + public override void Serialize(NetDataWriter writer) + { + writer.Put(PPDEnabled); + writer.Put(PPMEnabled); + } + } +} diff --git a/MultiplayerCore/UI/LobbySettingsViewController.bsml b/MultiplayerCore/UI/LobbySettingsViewController.bsml new file mode 100644 index 0000000..d607eb5 --- /dev/null +++ b/MultiplayerCore/UI/LobbySettingsViewController.bsml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MultiplayerCore/UI/LobbySettingsViewController.cs b/MultiplayerCore/UI/LobbySettingsViewController.cs new file mode 100644 index 0000000..ad2da57 --- /dev/null +++ b/MultiplayerCore/UI/LobbySettingsViewController.cs @@ -0,0 +1,64 @@ +using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage.Components.Settings; +using BeatSaberMarkupLanguage.ViewControllers; +using IPA.Utilities; +using Zenject; + +namespace MultiplayerCore.UI +{ + [ViewDefinition("MultiplayerCore.UI.LobbySettingsViewController.bsml")] + public class LobbySettingsViewController : BSMLAutomaticViewController + { + private FieldAccessor.Accessor _showModifiers + = FieldAccessor.GetAccessor(nameof(_showModifiers)); + private FieldAccessor.Accessor _showEnvironmentOverrideSettings + = FieldAccessor.GetAccessor(nameof(_showEnvironmentOverrideSettings)); + private FieldAccessor.Accessor _showColorSchemesSettings + = FieldAccessor.GetAccessor(nameof(_showColorSchemesSettings)); + private FieldAccessor.Accessor _showMultiplayer + = FieldAccessor.GetAccessor(nameof(_showMultiplayer)); + + private GameplaySetupViewController _gameplaySetup = null!; + private bool _perPlayerDiffs = false; + private bool _perPlayerModifiers = false; + //private Config _config = null!; + + [Inject] + private void Construct( + GameplaySetupViewController gameplaySetup/*, + Config config*/) + { + _gameplaySetup = gameplaySetup; + //_config = config; + } + + [UIAction("#post-parse")] + private void PostParse() + { + //_sideBySideDistanceIncrement.interactable = _sideBySide; + } + + [UIValue("per-player-diffs")] + public bool PerPlayerDifficulty + { + //get => _config.SoloEnvironment; + get => _perPlayerDiffs; + set + { + _perPlayerDiffs = value; + NotifyPropertyChanged(); + } + } + + [UIValue("per-player-modifiers")] + public bool PerPlayerModifiers + { + get => _perPlayerModifiers; + set + { + _perPlayerModifiers = value; + NotifyPropertyChanged(); + } + } + } +} diff --git a/MultiplayerCore/UI/LobbySetupPanel.bsml b/MultiplayerCore/UI/LobbySetupPanel.bsml new file mode 100644 index 0000000..57105ab --- /dev/null +++ b/MultiplayerCore/UI/LobbySetupPanel.bsml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/MultiplayerCore/UI/LobbySetupPanel.cs b/MultiplayerCore/UI/LobbySetupPanel.cs new file mode 100644 index 0000000..b622f40 --- /dev/null +++ b/MultiplayerCore/UI/LobbySetupPanel.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage.Components.Settings; +using BeatSaberMarkupLanguage.ViewControllers; +using HMUI; +using MultiplayerCore.Beatmaps.Providers; +using UnityEngine; +using Zenject; + +namespace MultiplayerCore.UI +{ + public class LobbySetupPanel : BSMLResourceViewController + { + public override string ResourceName => "MultiplayerCore.UI.LobbySetupPanel.bsml"; + private LobbySetupViewController _lobbyViewController; + private MpBeatmapLevelProvider _beatmapLevelProvider; + private ILobbyGameStateController _gameStateController; + private bool _perPlayerDiffs = false; + private bool _perPlayerModifiers = false; + + //BeatSaberMarkupLanguage.Tags.TextSegmentedControlTag + + [Inject] + internal void Inject(LobbySetupViewController lobbyViewController, MpBeatmapLevelProvider beatmapLevelProvider, ILobbyGameStateController gameStateController) + { + DidActivate(true, false, false); + _lobbyViewController = lobbyViewController; + _beatmapLevelProvider = beatmapLevelProvider; + _gameStateController = gameStateController; + + _lobbyViewController.didActivateEvent += DidActivate; + _lobbyViewController.didDeactivateEvent += DidDeactivate; + } + + protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) + { + Plugin.Logger.Debug($"LobbySetup DidActivate called!"); + base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); + + if (_gameStateController == null || _lobbyViewController == null || perPlayerDiffsToggle == null || + perPlayerModifiersToggle == null) + { + Plugin.Logger.Debug($"One object was null {_gameStateController}, {_lobbyViewController}, {perPlayerDiffsToggle}, {perPlayerModifiersToggle}"); + return; + } + + _gameStateController.lobbyStateChangedEvent += SetLobbyState; + + if (_lobbyViewController._isPartyOwner) + { + perPlayerDiffsToggle.gameObject.SetActive(true); + perPlayerModifiersToggle.gameObject.SetActive(true); + perPlayerDiffsToggle.interactable = true; + perPlayerModifiersToggle.interactable = true; + } + else + { + perPlayerDiffsToggle.gameObject.SetActive(false); + perPlayerModifiersToggle.gameObject.SetActive(false); + } + } + + protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) + { + base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); + Plugin.Logger.Debug($"LobbySetup DidDeactive called!"); + _gameStateController.lobbyStateChangedEvent -= SetLobbyState; + } + + private void SetLobbyState(MultiplayerLobbyState lobbyState) + { + if (_lobbyViewController == null) + return; + + if (_lobbyViewController._isPartyOwner) + { + perPlayerDiffsToggle.interactable = true; + perPlayerDiffsToggle.gameObject.SetActive(lobbyState == MultiplayerLobbyState.LobbySetup); + + perPlayerModifiersToggle.interactable = true; + perPlayerModifiersToggle.gameObject.SetActive(lobbyState == MultiplayerLobbyState.LobbySetup); + } + difficulty.enabled = lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown; + } + + + #region UIComponents + + [UIComponent("ppdt")] + public ToggleSetting perPlayerDiffsToggle; + + [UIComponent("ppmt")] + public ToggleSetting perPlayerModifiersToggle; + + [UIComponent("difficulty-control")] + public TextSegmentedControl difficulty; + + #endregion + + #region UIValues + + [UIValue("per-player-diffs")] + public bool PerPlayerDifficulty + { + get => _perPlayerDiffs; + set + { + _perPlayerDiffs = value; + NotifyPropertyChanged(); + } + } + + [UIValue("per-player-modifiers")] + public bool PerPlayerModifiers + { + get => _perPlayerModifiers; + set + { + _perPlayerModifiers = value; + NotifyPropertyChanged(); + } + } + + [UIValue("diffs")] + private List diffList { get; set; } = new() { "Easy", "Normal", "Hard", "Expert", "Expert+" }; + + [UIAction("difficulty-selected")] + public void SetSelectedDifficulty(TextSegmentedControl _, int index) + { + Plugin.Logger.Debug($"Selected difficulty {index}"); + } + #endregion + } +} diff --git a/MultiplayerCore/UI/MpCoreGameplaySetup.bsml b/MultiplayerCore/UI/MpCoreGameplaySetup.bsml new file mode 100644 index 0000000..6c4ed65 --- /dev/null +++ b/MultiplayerCore/UI/MpCoreGameplaySetup.bsml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MultiplayerCore/UI/MpCoreGameplaySetup.cs b/MultiplayerCore/UI/MpCoreGameplaySetup.cs new file mode 100644 index 0000000..44dbd41 --- /dev/null +++ b/MultiplayerCore/UI/MpCoreGameplaySetup.cs @@ -0,0 +1,104 @@ +using BeatSaberMarkupLanguage; +using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage.Components; +using HMUI; +using SiraUtil.Logging; +using System; +using System.Reflection; +using UnityEngine; +using Zenject; + +namespace MultiplayerCore.UI +{ + public class MpCoreGameplaySetup : NotifiableBase, IInitializable, IDisposable + { + public const string ResourcePath = "MultiplayerCore.UI.MpCoreGameplaySetup.bsml"; + + private GameplaySetupViewController _gameplaySetup; + private MultiplayerSettingsPanelController _multiplayerSettingsPanel; + private MainFlowCoordinator _mainFlowCoordinator; + private MpCoreSetupFlowCoordinator _setupFlowCoordinator; + //private readonly Config _config; + private SiraLog _logger; + private bool _perPlayerDiffs = false; + private bool _perPlayerModifiers = false; + + internal MpCoreGameplaySetup( + GameplaySetupViewController gameplaySetup, + MainFlowCoordinator mainFlowCoordinator, + MpCoreSetupFlowCoordinator setupFlowCoordinator, + //Config config, + SiraLog logger) + { + _gameplaySetup = gameplaySetup; + _multiplayerSettingsPanel = gameplaySetup._multiplayerSettingsPanelController; + _mainFlowCoordinator = mainFlowCoordinator; + _setupFlowCoordinator = setupFlowCoordinator; + //_config = config; + _logger = logger; + } + + public void Initialize() + { + BSMLParser.instance.Parse(BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _multiplayerSettingsPanel.gameObject, this); + while (0 < _vert.transform.childCount) + _vert.transform.GetChild(0).SetParent(_multiplayerSettingsPanel.transform); + } + + public void Dispose() + { + + } + + [UIAction("#post-parse")] + private void PostParse() + { + + } + + + [UIAction("preferences-click")] + private void PresentPreferences() + { + FlowCoordinator deepestChildFlowCoordinator = DeepestChildFlowCoordinator(_mainFlowCoordinator); + _setupFlowCoordinator.parentFlowCoordinator = deepestChildFlowCoordinator; + deepestChildFlowCoordinator.PresentFlowCoordinator(_setupFlowCoordinator); + } + + private FlowCoordinator DeepestChildFlowCoordinator(FlowCoordinator root) + { + var flow = root.childFlowCoordinator; + if (flow == null) return root; + if (flow.childFlowCoordinator == null || flow.childFlowCoordinator == flow) + { + return flow; + } + return DeepestChildFlowCoordinator(flow); + } + + [UIObject("vert")] + private GameObject _vert = null!; + + [UIValue("per-player-diffs")] + public bool PerPlayerDifficulty + { + get => _perPlayerDiffs; + set + { + _perPlayerDiffs = value; + NotifyPropertyChanged(); + } + } + + [UIValue("per-player-modifiers")] + public bool PerPlayerModifiers + { + get => _perPlayerModifiers; + set + { + _perPlayerModifiers = value; + NotifyPropertyChanged(); + } + } + } +} diff --git a/MultiplayerCore/UI/MpCoreSetupFlowCoordinator.cs b/MultiplayerCore/UI/MpCoreSetupFlowCoordinator.cs new file mode 100644 index 0000000..f6c8e0d --- /dev/null +++ b/MultiplayerCore/UI/MpCoreSetupFlowCoordinator.cs @@ -0,0 +1,62 @@ +using HMUI; +using Zenject; +using BeatSaberMarkupLanguage; +using SiraUtil.Affinity; +using System; + +namespace MultiplayerCore.UI +{ + public class MpCoreSetupFlowCoordinator : FlowCoordinator + { + internal FlowCoordinator parentFlowCoordinator = null!; + //private MpexSettingsViewController _settingsViewController = null!; + //private MpexEnvironmentViewController _environmentViewController = null!; + //private MpexMiscViewController _miscViewController = null!; + private ILobbyGameStateController _gameStateController = null!; + + [Inject] + public void Construct( + MainFlowCoordinator mainFlowCoordinator, + //MpexSettingsViewController settingsViewController, + //MpexEnvironmentViewController environmentViewController, + //MpexMiscViewController miscViewController, + ILobbyGameStateController gameStateController) + { + parentFlowCoordinator = mainFlowCoordinator; + //_settingsViewController = settingsViewController; + //_environmentViewController = environmentViewController; + //_miscViewController = miscViewController; + _gameStateController = gameStateController; + } + + protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) + { + if (firstActivation) + { + SetTitle("Lobby Preferences"); + showBackButton = true; + } + if (addedToHierarchy) + { + //ProvideInitialViewControllers(_settingsViewController, _environmentViewController, _miscViewController); + _gameStateController.gameStartedEvent += DismissGameStartedEvent; + } + } + + protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) + { + if (removedFromHierarchy) + _gameStateController.gameStartedEvent -= DismissGameStartedEvent; + } + + private void DismissGameStartedEvent(ILevelGameplaySetupData obj) + { + parentFlowCoordinator.DismissFlowCoordinator(this, null, ViewController.AnimationDirection.Horizontal, true); + } + + protected override void BackButtonWasPressed(ViewController topViewController) + { + parentFlowCoordinator.DismissFlowCoordinator(this); + } + } +} From 8bedef83e90afd1bbd77ae2a38e36fd91b9389c5 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Fri, 12 Jul 2024 23:00:26 +0200 Subject: [PATCH 30/75] Adjust UI, move start/unready button down --- MultiplayerCore/UI/LobbySetupPanel.bsml | 10 +++++++--- MultiplayerCore/UI/LobbySetupPanel.cs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/MultiplayerCore/UI/LobbySetupPanel.bsml b/MultiplayerCore/UI/LobbySetupPanel.bsml index 57105ab..e7a27f2 100644 --- a/MultiplayerCore/UI/LobbySetupPanel.bsml +++ b/MultiplayerCore/UI/LobbySetupPanel.bsml @@ -1,10 +1,14 @@ - + - - + + + + + + diff --git a/MultiplayerCore/UI/LobbySetupPanel.cs b/MultiplayerCore/UI/LobbySetupPanel.cs index b622f40..7978e95 100644 --- a/MultiplayerCore/UI/LobbySetupPanel.cs +++ b/MultiplayerCore/UI/LobbySetupPanel.cs @@ -34,6 +34,22 @@ internal void Inject(LobbySetupViewController lobbyViewController, MpBeatmapLeve _lobbyViewController.didActivateEvent += DidActivate; _lobbyViewController.didDeactivateEvent += DidDeactivate; + + var cgubPos = _lobbyViewController._cancelGameUnreadyButton.transform.position; + cgubPos.y -= 0.4f; + _lobbyViewController._cancelGameUnreadyButton.transform.position = cgubPos; + + var sgrbPos = _lobbyViewController._startGameReadyButton.transform.position; + sgrbPos.y -= 0.4f; + _lobbyViewController._startGameReadyButton.transform.position = sgrbPos; + + var csgHH = _lobbyViewController._cantStartGameHoverHint.transform.position; + csgHH.y -= 0.4f; + _lobbyViewController._cantStartGameHoverHint.transform.position = csgHH; + + //var stwParentPos = _lobbyViewController._spectatorWarningTextWrapper.transform.position; + //stwParentPos.y -= 0.5f; + //_lobbyViewController._spectatorWarningTextWrapper.transform.position = stwParentPos; } protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) From fe322aa9b667016aa20fd7f03f2cf4a4a0a91bdc Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 13 Jul 2024 22:57:47 +0200 Subject: [PATCH 31/75] Rename packets --- .../Packets/{GetPerPlayer.cs => GetMpPerPlayerPacket.cs} | 3 ++- .../Players/Packets/{PerPlayer.cs => MpPerPlayerPacket.cs} | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) rename MultiplayerCore/Players/Packets/{GetPerPlayer.cs => GetMpPerPlayerPacket.cs} (79%) rename MultiplayerCore/Players/Packets/{PerPlayer.cs => MpPerPlayerPacket.cs} (87%) diff --git a/MultiplayerCore/Players/Packets/GetPerPlayer.cs b/MultiplayerCore/Players/Packets/GetMpPerPlayerPacket.cs similarity index 79% rename from MultiplayerCore/Players/Packets/GetPerPlayer.cs rename to MultiplayerCore/Players/Packets/GetMpPerPlayerPacket.cs index b158e14..e8397fa 100644 --- a/MultiplayerCore/Players/Packets/GetPerPlayer.cs +++ b/MultiplayerCore/Players/Packets/GetMpPerPlayerPacket.cs @@ -5,10 +5,11 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using MultiplayerCore.Networking.Attributes; namespace MultiplayerCore.Players.Packets { - internal class GetPerPlayer : MpPacket + internal class GetMpPerPlayerPacket : MpPacket { public override void Deserialize(NetDataReader reader) { } diff --git a/MultiplayerCore/Players/Packets/PerPlayer.cs b/MultiplayerCore/Players/Packets/MpPerPlayerPacket.cs similarity index 87% rename from MultiplayerCore/Players/Packets/PerPlayer.cs rename to MultiplayerCore/Players/Packets/MpPerPlayerPacket.cs index 11649bc..bbf1673 100644 --- a/MultiplayerCore/Players/Packets/PerPlayer.cs +++ b/MultiplayerCore/Players/Packets/MpPerPlayerPacket.cs @@ -5,10 +5,11 @@ using System.Threading.Tasks; using LiteNetLib.Utils; using MultiplayerCore.Networking.Abstractions; +using MultiplayerCore.Networking.Attributes; namespace MultiplayerCore.Players.Packets { - internal class PerPlayer : MpPacket + internal class MpPerPlayerPacket : MpPacket { public bool PPDEnabled; public bool PPMEnabled; From a2a29c6c48c40146fcc17e1aa05d7d23de42d032 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 14 Jul 2024 17:17:17 +0200 Subject: [PATCH 32/75] Add a RegisterType function to register a type without callback, mostly useful for testing --- .../Networking/MpPacketSerializer.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/MultiplayerCore/Networking/MpPacketSerializer.cs b/MultiplayerCore/Networking/MpPacketSerializer.cs index 7352082..1bf65b6 100644 --- a/MultiplayerCore/Networking/MpPacketSerializer.cs +++ b/MultiplayerCore/Networking/MpPacketSerializer.cs @@ -90,6 +90,18 @@ public bool HandlesType(Type type) return registeredTypes.Contains(type); } + /// + /// Registers a packet without callback + /// + /// Type of packet to register. Inherits + public void RegisterType() + { + var packetType = typeof(TPacket); + var packetIdAttribute = packetType.GetCustomAttribute(); + var packetId = packetIdAttribute is not null ? packetIdAttribute.ID : packetType.Name; + registeredTypes.Add(packetType); + } + /// /// Registers a callback without sender for a packet. /// @@ -107,13 +119,13 @@ public bool HandlesType(Type type) /// public void RegisterCallback(Action callback) where TPacket : INetSerializable, new() { - var packetType = typeof(TPacket); - registeredTypes.Add(packetType); + var packetType = typeof(TPacket); + registeredTypes.Add(packetType); - var packetIdAttribute = packetType.GetCustomAttribute(); - var packetId = packetIdAttribute is not null ? packetIdAttribute.ID : packetType.Name; + var packetIdAttribute = packetType.GetCustomAttribute(); + var packetId = packetIdAttribute is not null ? packetIdAttribute.ID : packetType.Name; - Func deserialize = delegate (NetDataReader reader, int size) + Func deserialize = delegate (NetDataReader reader, int size) { TPacket packet = new TPacket(); if (packet == null) From 66f8b4eb6f9089b5c55bd51b44f356832969ff3c Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 14 Jul 2024 17:18:02 +0200 Subject: [PATCH 33/75] Working POC UI for ppd/ppm --- MultiplayerCore/UI/LobbySetupPanel.bsml | 8 +- MultiplayerCore/UI/LobbySetupPanel.cs | 156 ++++++++++++++++++++++-- 2 files changed, 149 insertions(+), 15 deletions(-) diff --git a/MultiplayerCore/UI/LobbySetupPanel.bsml b/MultiplayerCore/UI/LobbySetupPanel.bsml index e7a27f2..31e21d7 100644 --- a/MultiplayerCore/UI/LobbySetupPanel.bsml +++ b/MultiplayerCore/UI/LobbySetupPanel.bsml @@ -1,9 +1,9 @@ - - + + - - + + diff --git a/MultiplayerCore/UI/LobbySetupPanel.cs b/MultiplayerCore/UI/LobbySetupPanel.cs index 7978e95..dfa2082 100644 --- a/MultiplayerCore/UI/LobbySetupPanel.cs +++ b/MultiplayerCore/UI/LobbySetupPanel.cs @@ -7,34 +7,52 @@ using BeatSaberMarkupLanguage.Components.Settings; using BeatSaberMarkupLanguage.ViewControllers; using HMUI; +using MultiplayerCore.Beatmaps.Abstractions; using MultiplayerCore.Beatmaps.Providers; +using MultiplayerCore.Networking; +using MultiplayerCore.Players.Packets; using UnityEngine; +using UnityEngine.PlayerLoop; +using UnityEngine.UI; using Zenject; +using static IPA.Logging.Logger; namespace MultiplayerCore.UI { public class LobbySetupPanel : BSMLResourceViewController { public override string ResourceName => "MultiplayerCore.UI.LobbySetupPanel.bsml"; + private GameServerLobbyFlowCoordinator _gameServerLobbyFlowCoordinator; private LobbySetupViewController _lobbyViewController; - private MpBeatmapLevelProvider _beatmapLevelProvider; + private BeatmapLevelsModel _beatmapLevelsModel; + private IMultiplayerSessionManager _multiplayerSessionManager; private ILobbyGameStateController _gameStateController; + private MpPacketSerializer _packetSerializer; + private MpBeatmapLevelProvider _beatmapLevelProvider; + private BeatmapKey _currentBeatmapKey; private bool _perPlayerDiffs = false; private bool _perPlayerModifiers = false; + private List _allowedDiffs; //BeatSaberMarkupLanguage.Tags.TextSegmentedControlTag [Inject] - internal void Inject(LobbySetupViewController lobbyViewController, MpBeatmapLevelProvider beatmapLevelProvider, ILobbyGameStateController gameStateController) + internal void Inject(GameServerLobbyFlowCoordinator gameServerLobbyFlowCoordinator, BeatmapLevelsModel beatmapLevelsModel, IMultiplayerSessionManager sessionManager, MpBeatmapLevelProvider beatmapLevelProvider, MpPacketSerializer packetSerializer) { DidActivate(true, false, false); - _lobbyViewController = lobbyViewController; + _gameServerLobbyFlowCoordinator = gameServerLobbyFlowCoordinator; + _lobbyViewController = _gameServerLobbyFlowCoordinator._lobbySetupViewController; + _gameStateController = _gameServerLobbyFlowCoordinator._lobbyGameStateController; + _beatmapLevelsModel = beatmapLevelsModel; + _multiplayerSessionManager = sessionManager; _beatmapLevelProvider = beatmapLevelProvider; - _gameStateController = gameStateController; + _packetSerializer = packetSerializer; _lobbyViewController.didActivateEvent += DidActivate; _lobbyViewController.didDeactivateEvent += DidDeactivate; + + // TODO: Possibly adjust position based on enabled UI elements var cgubPos = _lobbyViewController._cancelGameUnreadyButton.transform.position; cgubPos.y -= 0.4f; _lobbyViewController._cancelGameUnreadyButton.transform.position = cgubPos; @@ -47,6 +65,8 @@ internal void Inject(LobbySetupViewController lobbyViewController, MpBeatmapLeve csgHH.y -= 0.4f; _lobbyViewController._cantStartGameHoverHint.transform.position = csgHH; + _packetSerializer.RegisterType(); + //var stwParentPos = _lobbyViewController._spectatorWarningTextWrapper.transform.position; //stwParentPos.y -= 0.5f; //_lobbyViewController._spectatorWarningTextWrapper.transform.position = stwParentPos; @@ -54,7 +74,7 @@ internal void Inject(LobbySetupViewController lobbyViewController, MpBeatmapLeve protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { - Plugin.Logger.Debug($"LobbySetup DidActivate called!"); + Plugin.Logger.Debug($"LobbySetup DidActivate called! {firstActivation}, {addedToHierarchy}, {screenSystemEnabling}"); base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); if (_gameStateController == null || _lobbyViewController == null || perPlayerDiffsToggle == null || @@ -63,8 +83,35 @@ protected override void DidActivate(bool firstActivation, bool addedToHierarchy, Plugin.Logger.Debug($"One object was null {_gameStateController}, {_lobbyViewController}, {perPlayerDiffsToggle}, {perPlayerModifiersToggle}"); return; } + // Make sure to only allow selecting difficulties that are enabled for the lobby + //var lobbyPlayersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as LobbyPlayersDataModel; + //LobbyPlayerData? playerData = + // lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, + // out _); + //if (playerData != null && playerData.beatmapKey.IsValid()) + // _currentBeatmapKey = playerData.beatmapKey; + //else if (_gameStateController.selectedLevelGameplaySetupData.beatmapKey.IsValid()) + // _currentBeatmapKey = _gameStateController.selectedLevelGameplaySetupData.beatmapKey; + //if (_currentBeatmapKey.IsValid()) UpdateDifficultyList(_currentBeatmapKey); + //else segmentVert.gameObject.SetActive(false); + // We register the callbacks after setting the initial values _gameStateController.lobbyStateChangedEvent += SetLobbyState; + //_gameStateController.selectedLevelGameplaySetupDataChangedEvent += HostSelectedBeatmap; + _gameServerLobbyFlowCoordinator._multiplayerLevelSelectionFlowCoordinator.didSelectLevelEvent += LocalSelectedBeatmap; + + //else UpdateDifficultyList(Enum.GetValues(typeof(BeatmapDifficulty)).Cast().ToList()); + + //List difficultyList = + // Enum.GetValues(typeof(BeatmapDifficulty)).Cast().ToList(); + //var levelId = _gameStateController.selectedLevelGameplaySetupData.beatmapKey.levelId; + //var characteristic = _gameStateController.selectedLevelGameplaySetupData.beatmapKey.beatmapCharacteristic; + //_allowedDiffs = (from diff in difficultyList + // where _gameServerLobbyFlowCoordinator._unifiedNetworkPlayerModel.selectionMask.difficulties + // .Contains(diff) + // select diff.ToString().Replace("ExpertPlus", "Expert+")).ToList(); + //foreach (var difficulty in _allowedDiffs) + // Plugin.Logger.Debug($"Allowed difficulty='{difficulty}'"); if (_lobbyViewController._isPartyOwner) { @@ -83,8 +130,58 @@ protected override void DidActivate(bool firstActivation, bool addedToHierarchy, protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) { base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); - Plugin.Logger.Debug($"LobbySetup DidDeactive called!"); - _gameStateController.lobbyStateChangedEvent -= SetLobbyState; + Plugin.Logger.Debug($"LobbySetup DidDeactivate called! {removedFromHierarchy}, {screenSystemDisabling}"); + if (removedFromHierarchy) + { + _gameStateController.lobbyStateChangedEvent -= SetLobbyState; + //_gameStateController.selectedLevelGameplaySetupDataChangedEvent -= HostSelectedBeatmap; + _gameServerLobbyFlowCoordinator._multiplayerLevelSelectionFlowCoordinator.didSelectLevelEvent -= LocalSelectedBeatmap; + } + } + + private void UpdateDifficultyList(BeatmapKey beatmapKey) + { + var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); + if (levelHash != null) + { + Plugin.Logger.Debug($"Level is custom, trying to get beatmap for hash {levelHash}"); + _beatmapLevelProvider.GetBeatmap(levelHash).ContinueWith(levelTask => + { + if (levelTask.IsCompleted && !levelTask.IsFaulted && levelTask.Result != null) + { + var level = levelTask.Result; + Plugin.Logger.Debug($"Got level {level.LevelHash}, {level.Requirements}, {level.Requirements[beatmapKey.beatmapCharacteristic.serializedName]}"); + // Hacky we use requirements to get the available difficulties + UpdateDifficultyList(level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Keys + .ToList()); + } + }); + } + else + { + Plugin.Logger.Debug($"LevelId not custom: {beatmapKey.levelId}, getting difficulties from basegame"); + UpdateDifficultyList(_beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId).GetDifficulties(beatmapKey.beatmapCharacteristic).ToList()); + } + } + + private void UpdateDifficultyList(IReadOnlyList difficulties) + { + _allowedDiffs = (from diff in difficulties + where _gameServerLobbyFlowCoordinator._unifiedNetworkPlayerModel.selectionMask.difficulties + .Contains(diff) + select diff.ToString().Replace("ExpertPlus", "Expert+") + ).ToList(); + foreach (var difficulty in _allowedDiffs) + Plugin.Logger.Debug($"Allowed difficulty='{difficulty}'"); + + if (_allowedDiffs.Count > 1) + { + Plugin.Logger.Debug($"Setting texts"); + difficulty.SetTexts(_allowedDiffs); + Plugin.Logger.Debug("Enabling gameObject"); + segmentVert.gameObject.SetActive(true); + } + else segmentVert.gameObject.SetActive(false); } private void SetLobbyState(MultiplayerLobbyState lobbyState) @@ -104,6 +201,24 @@ private void SetLobbyState(MultiplayerLobbyState lobbyState) lobbyState == MultiplayerLobbyState.LobbyCountdown; } + private void LocalSelectedBeatmap(LevelSelectionFlowCoordinator.State state) + { + _currentBeatmapKey = state.beatmapKey; + UpdateDifficultyList(state.beatmapLevel.GetDifficulties(state.beatmapKey.beatmapCharacteristic).ToList()); + difficulty.SelectCellWithNumber(_allowedDiffs.IndexOf(_currentBeatmapKey.difficulty.ToString().Replace("ExpertPlus", "Expert+"))); + //var lobbyPlayersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as LobbyPlayersDataModel; + //LobbyPlayerData? playerData = + // lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, + // out _); + //if (playerData != null && playerData.beatmapKey.IsValid()) + // UpdateDifficultyList(playerData.beatmapKey); + } + + private void HostSelectedBeatmap(ILevelGameplaySetupData gameplaySetupData) + { + UpdateDifficultyList(gameplaySetupData.beatmapKey); + } + #region UIComponents @@ -116,6 +231,9 @@ private void SetLobbyState(MultiplayerLobbyState lobbyState) [UIComponent("difficulty-control")] public TextSegmentedControl difficulty; + [UIComponent("segment-vert")] + public VerticalLayoutGroup segmentVert; + #endregion #region UIValues @@ -127,6 +245,12 @@ public bool PerPlayerDifficulty set { _perPlayerDiffs = value; + _multiplayerSessionManager.Send(new MpPerPlayerPacket + { + PPDEnabled = _perPlayerDiffs, + PPMEnabled = _perPlayerModifiers + }); + Plugin.Logger.Debug($"Sending MpPerPlayerPacket Packet with values: PPDEnabled='{_perPlayerDiffs}', PPMEnabled='{_perPlayerModifiers}'"); NotifyPropertyChanged(); } } @@ -138,17 +262,27 @@ public bool PerPlayerModifiers set { _perPlayerModifiers = value; + _multiplayerSessionManager.Send(new MpPerPlayerPacket + { + PPDEnabled = _perPlayerDiffs, + PPMEnabled = _perPlayerModifiers + }); + Plugin.Logger.Debug($"Sending MpPerPlayerPacket Packet with values: PPDEnabled='{_perPlayerDiffs}', PPMEnabled='{_perPlayerModifiers}'"); NotifyPropertyChanged(); } } - [UIValue("diffs")] - private List diffList { get; set; } = new() { "Easy", "Normal", "Hard", "Expert", "Expert+" }; - [UIAction("difficulty-selected")] public void SetSelectedDifficulty(TextSegmentedControl _, int index) { - Plugin.Logger.Debug($"Selected difficulty {index}"); + var diff = _allowedDiffs[index]; + Plugin.Logger.Debug($"Selected difficulty at {index} - {diff}"); + if (Enum.TryParse(diff.Replace("Expert+", "ExpertPlus"), out BeatmapDifficulty difficulty)) + { + _currentBeatmapKey = new BeatmapKey(_currentBeatmapKey.levelId, + _currentBeatmapKey.beatmapCharacteristic, difficulty); + _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel.SetLocalPlayerBeatmapLevel(_currentBeatmapKey); + } } #endregion } From acf38efbea6318a183b162678f0b04c60317da33 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 14 Jul 2024 18:03:02 +0200 Subject: [PATCH 34/75] Add support for 1.37.1 with backwards compat --- MultiplayerCore/Objects/MpLevelLoader.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index a45cd22..53a9f20 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using HarmonyLib; using JetBrains.Annotations; using SiraUtil.Logging; @@ -113,8 +114,20 @@ private async Task DownloadBeatmapLevelAsync(string } // Load level data - var loadResult = await _beatmapLevelsModel.LoadBeatmapLevelDataAsync(levelId, BeatmapLevelDataVersion.Original, cancellationToken); - if (loadResult.isError) + var method = AccessTools.Method(_beatmapLevelsModel.GetType(), nameof(_beatmapLevelsModel.LoadBeatmapLevelDataAsync)); + LoadBeatmapLevelDataResult loadResult; + if (method != null) + { + if (method.GetParameters().Length > 2) + loadResult = await (Task)method.Invoke(_beatmapLevelsModel, + new object[] { levelId, BeatmapLevelDataVersion.Original, cancellationToken }); + else if (method.GetParameters().Length == 2) + loadResult = await (Task)method.Invoke(_beatmapLevelsModel, + new object[] { levelId, cancellationToken }); + else throw new NotSupportedException("Game version not supported"); + } + //var loadResult = await _beatmapLevelsModel.LoadBeatmapLevelDataAsync(levelId, BeatmapLevelDataVersion.Original, cancellationToken); + if (loadResult.isError) _logger.Error($"Custom level data could not be loaded after download: {levelId}"); return loadResult; } From 1e6cde3f38bf90a5b6626bad675bc1123e94b12e Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 14 Jul 2024 19:17:48 +0200 Subject: [PATCH 35/75] Fix potential null dereference --- MultiplayerCore/Objects/MpLevelLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index 53a9f20..d447a42 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -115,7 +115,7 @@ private async Task DownloadBeatmapLevelAsync(string // Load level data var method = AccessTools.Method(_beatmapLevelsModel.GetType(), nameof(_beatmapLevelsModel.LoadBeatmapLevelDataAsync)); - LoadBeatmapLevelDataResult loadResult; + LoadBeatmapLevelDataResult loadResult = LoadBeatmapLevelDataResult.Error; if (method != null) { if (method.GetParameters().Length > 2) From e5224b24e2b2ca386fba9c036361bc60748d98a4 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Tue, 16 Jul 2024 19:26:26 +0200 Subject: [PATCH 36/75] Update difficulty selection when selecting suggestion, set alpha on difficulty selection at game start --- MultiplayerCore/UI/LobbySetupPanel.bsml | 11 +- MultiplayerCore/UI/LobbySetupPanel.cs | 211 +++++++++++++++++++----- 2 files changed, 178 insertions(+), 44 deletions(-) diff --git a/MultiplayerCore/UI/LobbySetupPanel.bsml b/MultiplayerCore/UI/LobbySetupPanel.bsml index 31e21d7..a6df7b7 100644 --- a/MultiplayerCore/UI/LobbySetupPanel.bsml +++ b/MultiplayerCore/UI/LobbySetupPanel.bsml @@ -1,9 +1,10 @@ - - - - + + + + + - + diff --git a/MultiplayerCore/UI/LobbySetupPanel.cs b/MultiplayerCore/UI/LobbySetupPanel.cs index dfa2082..f851237 100644 --- a/MultiplayerCore/UI/LobbySetupPanel.cs +++ b/MultiplayerCore/UI/LobbySetupPanel.cs @@ -7,6 +7,7 @@ using BeatSaberMarkupLanguage.Components.Settings; using BeatSaberMarkupLanguage.ViewControllers; using HMUI; +using IPA.Utilities.Async; using MultiplayerCore.Beatmaps.Abstractions; using MultiplayerCore.Beatmaps.Providers; using MultiplayerCore.Networking; @@ -33,6 +34,7 @@ public class LobbySetupPanel : BSMLResourceViewController private bool _perPlayerDiffs = false; private bool _perPlayerModifiers = false; private List _allowedDiffs; + private CanvasGroup? _difficultyCanvasGroup; //BeatSaberMarkupLanguage.Tags.TextSegmentedControlTag @@ -61,11 +63,20 @@ internal void Inject(GameServerLobbyFlowCoordinator gameServerLobbyFlowCoordinat sgrbPos.y -= 0.4f; _lobbyViewController._startGameReadyButton.transform.position = sgrbPos; - var csgHH = _lobbyViewController._cantStartGameHoverHint.transform.position; - csgHH.y -= 0.4f; - _lobbyViewController._cantStartGameHoverHint.transform.position = csgHH; + var csgHHPos = _lobbyViewController._cantStartGameHoverHint.transform.position; + csgHHPos.y -= 0.4f; + _lobbyViewController._cantStartGameHoverHint.transform.position = csgHHPos; - _packetSerializer.RegisterType(); + //if (!_lobbyViewController._isPartyOwner) + //{ + // var diffPos = difficulty.transform.position; + // diffPos.y -= -0.2f; + + //} + + // TODO: Proper registration + _packetSerializer.RegisterCallback(HandleMpPerPlayerPacket); + _packetSerializer.RegisterType(); //var stwParentPos = _lobbyViewController._spectatorWarningTextWrapper.transform.position; //stwParentPos.y -= 0.5f; @@ -84,21 +95,38 @@ protected override void DidActivate(bool firstActivation, bool addedToHierarchy, return; } // Make sure to only allow selecting difficulties that are enabled for the lobby - //var lobbyPlayersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as LobbyPlayersDataModel; - //LobbyPlayerData? playerData = - // lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, - // out _); - //if (playerData != null && playerData.beatmapKey.IsValid()) - // _currentBeatmapKey = playerData.beatmapKey; + if (!firstActivation) + { + var lobbyPlayersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as LobbyPlayersDataModel; + LobbyPlayerData? playerData = + lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, + out _); + if (playerData != null) + UpdateDifficultyList(playerData.beatmapKey); + } + + if (!firstActivation && addedToHierarchy) + { + // Reset our buttons + _perPlayerDiffs = false; + _perPlayerModifiers = false; + UpdateButtonValues(); + // Request Updated state + _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); + } //else if (_gameStateController.selectedLevelGameplaySetupData.beatmapKey.IsValid()) // _currentBeatmapKey = _gameStateController.selectedLevelGameplaySetupData.beatmapKey; //if (_currentBeatmapKey.IsValid()) UpdateDifficultyList(_currentBeatmapKey); //else segmentVert.gameObject.SetActive(false); - // We register the callbacks after setting the initial values + // We register the callbacks _gameStateController.lobbyStateChangedEvent += SetLobbyState; //_gameStateController.selectedLevelGameplaySetupDataChangedEvent += HostSelectedBeatmap; _gameServerLobbyFlowCoordinator._multiplayerLevelSelectionFlowCoordinator.didSelectLevelEvent += LocalSelectedBeatmap; + _gameServerLobbyFlowCoordinator._serverPlayerListViewController.selectSuggestedBeatmapEvent += UpdateDifficultyList; + _lobbyViewController.clearSuggestedBeatmapEvent += ClearLocalSelectedBeatmap; + _gameServerLobbyFlowCoordinator._lobbyPlayerPermissionsModel.permissionsChangedEvent += + UpdateButtonsEnabled; //else UpdateDifficultyList(Enum.GetValues(typeof(BeatmapDifficulty)).Cast().ToList()); @@ -136,11 +164,42 @@ protected override void DidDeactivate(bool removedFromHierarchy, bool screenSyst _gameStateController.lobbyStateChangedEvent -= SetLobbyState; //_gameStateController.selectedLevelGameplaySetupDataChangedEvent -= HostSelectedBeatmap; _gameServerLobbyFlowCoordinator._multiplayerLevelSelectionFlowCoordinator.didSelectLevelEvent -= LocalSelectedBeatmap; + _gameServerLobbyFlowCoordinator._serverPlayerListViewController.selectSuggestedBeatmapEvent -= UpdateDifficultyList; + _lobbyViewController.clearSuggestedBeatmapEvent -= ClearLocalSelectedBeatmap; + _gameServerLobbyFlowCoordinator._lobbyPlayerPermissionsModel.permissionsChangedEvent -= + UpdateButtonsEnabled; } } + private void HandleMpPerPlayerPacket(MpPerPlayerPacket packet, IConnectedPlayer player) + { + Plugin.Logger.Debug($"Got MpPerPlayerPacket from {player.userName}|{player.userId} with values PPDEnabled={packet.PPDEnabled}, PPMEnabled={packet.PPMEnabled}"); + if (packet.PPDEnabled != PerPlayerDifficulty || packet.PPMEnabled != _perPlayerModifiers) + { + _perPlayerDiffs = packet.PPDEnabled; + _perPlayerModifiers = packet.PPMEnabled; + //perPlayerDiffsToggle.Value = _perPlayerDiffs; + //perPlayerModifiersToggle.Value = _perPlayerModifiers; + UpdateButtonValues(); + } + } + + public string DiffToStr(BeatmapDifficulty difficulty) + { + return difficulty == BeatmapDifficulty.ExpertPlus ? "Expert+" : difficulty.ToString(); + } + + //public List DiffsToStrs(BeatmapDifficulty[] difficulties) => + // difficulties.Select(diff => DiffToStr(diff)).ToList(); + private void UpdateDifficultyList(BeatmapKey beatmapKey) { + _currentBeatmapKey = beatmapKey; + if (!_currentBeatmapKey.IsValid()) + { + segmentVert.gameObject.SetActive(false); + return; + } var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); if (levelHash != null) { @@ -160,52 +219,77 @@ private void UpdateDifficultyList(BeatmapKey beatmapKey) else { Plugin.Logger.Debug($"LevelId not custom: {beatmapKey.levelId}, getting difficulties from basegame"); - UpdateDifficultyList(_beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId).GetDifficulties(beatmapKey.beatmapCharacteristic).ToList()); + var diffList = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId) + ?.GetDifficulties(beatmapKey.beatmapCharacteristic).ToList(); + if (diffList != null) UpdateDifficultyList(diffList); } } private void UpdateDifficultyList(IReadOnlyList difficulties) { - _allowedDiffs = (from diff in difficulties - where _gameServerLobbyFlowCoordinator._unifiedNetworkPlayerModel.selectionMask.difficulties - .Contains(diff) - select diff.ToString().Replace("ExpertPlus", "Expert+") - ).ToList(); - foreach (var difficulty in _allowedDiffs) - Plugin.Logger.Debug($"Allowed difficulty='{difficulty}'"); - - if (_allowedDiffs.Count > 1) - { - Plugin.Logger.Debug($"Setting texts"); - difficulty.SetTexts(_allowedDiffs); - Plugin.Logger.Debug("Enabling gameObject"); - segmentVert.gameObject.SetActive(true); - } - else segmentVert.gameObject.SetActive(false); + // Ensure that we run on the UnityMainThread here + UnityMainThreadTaskScheduler.Factory.StartNew(() => + { + _allowedDiffs = (from diff in difficulties + where _gameServerLobbyFlowCoordinator._unifiedNetworkPlayerModel.selectionMask.difficulties + .Contains(diff) + select DiffToStr(diff) + ).ToList(); + foreach (var difficultyStr in _allowedDiffs) + Plugin.Logger.Debug($"Allowed difficulty='{difficultyStr}'"); + + if (_allowedDiffs.Count > 1) + { + segmentVert.gameObject.SetActive(true); + difficulty.SetTexts(_allowedDiffs); + int index = _allowedDiffs.IndexOf(DiffToStr(_currentBeatmapKey.difficulty)); + if (index > 0) + difficulty.SelectCellWithNumber(index); + } + else segmentVert.gameObject.SetActive(false); + } + ); } private void SetLobbyState(MultiplayerLobbyState lobbyState) { + Plugin.Logger.Debug($"Current Lobby State {lobbyState}"); + enableUserInteractions = lobbyState == MultiplayerLobbyState.LobbySetup; + + if (_difficultyCanvasGroup == null) + _difficultyCanvasGroup = difficulty?.gameObject.AddComponent(); + if (_difficultyCanvasGroup != null) + _difficultyCanvasGroup.alpha = lobbyState == MultiplayerLobbyState.LobbySetup ? 1f : 0.25f; + //var canvasGroup = segmentVert.GetComponentInParent(); + //var ourCanvasGroup = GetComponent(); + //if (ourCanvasGroup != null) + // Plugin.Logger.Debug($"CanvasGroup found in parent"); + //else Plugin.Logger.Error($"CanvasGroup was null!"); + //if (ourCanvasGroup != null) + // //UnityMainThreadTaskScheduler.Factory.StartNew(async () => + // //{ + // //await Task.Delay(2000); + // ourCanvasGroup.alpha = lobbyState == MultiplayerLobbyState.LobbySetup ? 1f : 0.25f; + // //}); + //segmentVert. = (lobbyState == MultiplayerLobbyState.LobbySetup); if (_lobbyViewController == null) return; if (_lobbyViewController._isPartyOwner) { - perPlayerDiffsToggle.interactable = true; - perPlayerDiffsToggle.gameObject.SetActive(lobbyState == MultiplayerLobbyState.LobbySetup); + perPlayerDiffsToggle.interactable = lobbyState == MultiplayerLobbyState.LobbySetup; + //perPlayerDiffsToggle.gameObject.SetActive(lobbyState == MultiplayerLobbyState.LobbySetup); + + perPlayerModifiersToggle.interactable = lobbyState == MultiplayerLobbyState.LobbySetup; + //perPlayerModifiersToggle.gameObject.SetActive(lobbyState == MultiplayerLobbyState.LobbySetup); - perPlayerModifiersToggle.interactable = true; - perPlayerModifiersToggle.gameObject.SetActive(lobbyState == MultiplayerLobbyState.LobbySetup); } - difficulty.enabled = lobbyState == MultiplayerLobbyState.LobbySetup || - lobbyState == MultiplayerLobbyState.LobbyCountdown; } private void LocalSelectedBeatmap(LevelSelectionFlowCoordinator.State state) { _currentBeatmapKey = state.beatmapKey; UpdateDifficultyList(state.beatmapLevel.GetDifficulties(state.beatmapKey.beatmapCharacteristic).ToList()); - difficulty.SelectCellWithNumber(_allowedDiffs.IndexOf(_currentBeatmapKey.difficulty.ToString().Replace("ExpertPlus", "Expert+"))); //var lobbyPlayersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as LobbyPlayersDataModel; //LobbyPlayerData? playerData = // lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, @@ -214,9 +298,35 @@ private void LocalSelectedBeatmap(LevelSelectionFlowCoordinator.State state) // UpdateDifficultyList(playerData.beatmapKey); } - private void HostSelectedBeatmap(ILevelGameplaySetupData gameplaySetupData) + //private void HostSelectedBeatmap(ILevelGameplaySetupData gameplaySetupData) + //{ + // UpdateDifficultyList(gameplaySetupData.beatmapKey); + //} + + private void ClearLocalSelectedBeatmap() { - UpdateDifficultyList(gameplaySetupData.beatmapKey); + segmentVert.gameObject.SetActive(false); + _currentBeatmapKey = new BeatmapKey(); + } + + private void UpdateButtonsEnabled() + { + if (_lobbyViewController._isPartyOwner) + { + perPlayerDiffsToggle.gameObject.SetActive(true); + perPlayerModifiersToggle.gameObject.SetActive(true); + } + else + { + perPlayerDiffsToggle.gameObject.SetActive(false); + perPlayerModifiersToggle.gameObject.SetActive(false); + } + } + + private void UpdateButtonValues() + { + perPlayerDiffsToggle.Value = _perPlayerDiffs; + perPlayerModifiersToggle.Value = _perPlayerModifiers; } @@ -250,7 +360,6 @@ public bool PerPlayerDifficulty PPDEnabled = _perPlayerDiffs, PPMEnabled = _perPlayerModifiers }); - Plugin.Logger.Debug($"Sending MpPerPlayerPacket Packet with values: PPDEnabled='{_perPlayerDiffs}', PPMEnabled='{_perPlayerModifiers}'"); NotifyPropertyChanged(); } } @@ -267,11 +376,35 @@ public bool PerPlayerModifiers PPDEnabled = _perPlayerDiffs, PPMEnabled = _perPlayerModifiers }); - Plugin.Logger.Debug($"Sending MpPerPlayerPacket Packet with values: PPDEnabled='{_perPlayerDiffs}', PPMEnabled='{_perPlayerModifiers}'"); NotifyPropertyChanged(); } } + //[UIAction("per-player-modifiers-changed")] + //public void OnPerPlayerModifiersChanged(bool value) + //{ + // _perPlayerModifiers = value; + // _multiplayerSessionManager.Send(new MpPerPlayerPacket + // { + // PPDEnabled = _perPlayerDiffs, + // PPMEnabled = _perPlayerModifiers + // }); + // Plugin.Logger.Debug($"Sending MpPerPlayerPacket Packet with values: PPDEnabled='{_perPlayerDiffs}', PPMEnabled='{_perPlayerModifiers}'"); + //} + + //[UIAction("per-player-diffs-changed")] + //public void OnPerPlayerDifficultyChanged(bool value) + //{ + // _perPlayerDiffs = value; + // _multiplayerSessionManager.Send(new MpPerPlayerPacket + // { + // PPDEnabled = _perPlayerDiffs, + // PPMEnabled = _perPlayerModifiers + // }); + // Plugin.Logger.Debug($"Sending MpPerPlayerPacket Packet with values: PPDEnabled='{_perPlayerDiffs}', PPMEnabled='{_perPlayerModifiers}'"); + //} + + [UIAction("difficulty-selected")] public void SetSelectedDifficulty(TextSegmentedControl _, int index) { From 9c3e55c35c8f47a189ce320a9efa3b86f447e28b Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 17 Jul 2024 07:43:25 +0200 Subject: [PATCH 37/75] Debug logging attempt getting id of failed packet --- MultiplayerCore/Patches/LoggingPatch.cs | 43 +++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Patches/LoggingPatch.cs b/MultiplayerCore/Patches/LoggingPatch.cs index 7041d8d..39aad82 100644 --- a/MultiplayerCore/Patches/LoggingPatch.cs +++ b/MultiplayerCore/Patches/LoggingPatch.cs @@ -44,8 +44,47 @@ private static void LogPacketError(NetDataReader reader, IConnectedPlayer p, Exc Plugin.Logger.Warn($"An exception was thrown processing a packet from player '{p?.userName ?? ""}|{p?.userId ?? " < NULL > "}': {ex.Message}"); Plugin.Logger.Debug(ex); #if (DEBUG) - Plugin.Logger.Error($"Errored packet: {BitConverter.ToString(reader.RawData)}"); + Plugin.Logger.Error($"Errored packet Postion={reader.Position}, RawDataSize={reader.RawDataSize} RawData: {BitConverter.ToString(reader.RawData)}"); + try + { + reader.SkipBytes(-reader.Position); + byte header1, header2, header3; + if (!reader.TryGetByte(out header1) || !reader.TryGetByte(out header2) || + !reader.TryGetByte(out header3) || reader.AvailableBytes == 0) + { + Plugin.Logger.Debug("Failed to get RoutingHeader"); + } + else + { + Plugin.Logger.Debug($"Routing Header bytes=({header1},{header2},{header3})"); + int index = 0; + while (!reader.EndOfData && index < 100) + { + Plugin.Logger.Debug($"Iteration='{index}' Attempt read data length from packet"); + int length = (int)reader.GetVarUInt(); + int subIteration = 0; + while (length > 0 && length <= reader.AvailableBytes && subIteration < 100) + { + Plugin.Logger.Debug($"Iteration='{index}' subIteration='{subIteration}' Length='{length}' AvailableBytes={reader.AvailableBytes}"); + byte packetId = reader.GetByte(); + length--; + Plugin.Logger.Debug($"Iteration='{index}' subIteration='{subIteration}' PacketId='{packetId}' RemainingLength='{length}'"); + subIteration++; + } + reader.SkipBytes(Math.Min(length, reader.AvailableBytes)); + Plugin.Logger.Debug($"Iteration='{index}' RemainingBytes='{reader.AvailableBytes}'"); + index++; + } + } + } + catch + { + } + finally + { + Plugin.Logger.Debug($"Finished Debug Logging for Packet!"); + } #endif - } + } } } From 26b2f28ff8848e58b580ae20d29aff5865876a71 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 17 Jul 2024 07:44:41 +0200 Subject: [PATCH 38/75] Allow changing difficulty during normal countdown --- MultiplayerCore/UI/LobbySetupPanel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/UI/LobbySetupPanel.cs b/MultiplayerCore/UI/LobbySetupPanel.cs index f851237..3188fd9 100644 --- a/MultiplayerCore/UI/LobbySetupPanel.cs +++ b/MultiplayerCore/UI/LobbySetupPanel.cs @@ -254,12 +254,14 @@ select DiffToStr(diff) private void SetLobbyState(MultiplayerLobbyState lobbyState) { Plugin.Logger.Debug($"Current Lobby State {lobbyState}"); - enableUserInteractions = lobbyState == MultiplayerLobbyState.LobbySetup; + enableUserInteractions = lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown; if (_difficultyCanvasGroup == null) _difficultyCanvasGroup = difficulty?.gameObject.AddComponent(); if (_difficultyCanvasGroup != null) - _difficultyCanvasGroup.alpha = lobbyState == MultiplayerLobbyState.LobbySetup ? 1f : 0.25f; + _difficultyCanvasGroup.alpha = (lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown) ? 1f : 0.25f; //var canvasGroup = segmentVert.GetComponentInParent(); //var ourCanvasGroup = GetComponent(); //if (ourCanvasGroup != null) @@ -315,6 +317,8 @@ private void UpdateButtonsEnabled() { perPlayerDiffsToggle.gameObject.SetActive(true); perPlayerModifiersToggle.gameObject.SetActive(true); + // Request updated button states from server + _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); } else { From c9be9bdab43386d79944400c7aaa2cd4c30f69ee Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 17 Jul 2024 07:46:21 +0200 Subject: [PATCH 39/75] Fix send MpBeatmapPacket logic for non-local levels --- MultiplayerCore/Objects/MpPlayersDataModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index 420e1f5..96d4c94 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -95,7 +95,7 @@ private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer base.HandleMenuRpcManagerRecommendBeatmap(userId, beatmapKeySerializable); } - private void SendMpBeatmapPacket(BeatmapKey beatmapKey) + private async void SendMpBeatmapPacket(BeatmapKey beatmapKey) { var levelId = beatmapKey.levelId; _logger.Debug($"Sending beatmap packet for level {levelId}"); @@ -107,7 +107,7 @@ private void SendMpBeatmapPacket(BeatmapKey beatmapKey) return; } - var levelData = _beatmapLevelProvider.GetBeatmapFromLocalBeatmaps(levelHash); + var levelData = await _beatmapLevelProvider.GetBeatmap(levelHash); if (levelData == null) { _logger.Debug("Could not get level data for beatmap, returning!"); From 4d9d1e361b78f5564fcd52dacefee77258b9649b Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 27 Jul 2024 22:06:10 +0200 Subject: [PATCH 40/75] Add Temporay patch for compatiblity between 1.37.1 and 1.37.0 --- MultiplayerCore/Patches/ForwardCompatHook.cs | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 MultiplayerCore/Patches/ForwardCompatHook.cs diff --git a/MultiplayerCore/Patches/ForwardCompatHook.cs b/MultiplayerCore/Patches/ForwardCompatHook.cs new file mode 100644 index 0000000..532f260 --- /dev/null +++ b/MultiplayerCore/Patches/ForwardCompatHook.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HarmonyLib; +using IPA.Utilities; +using LiteNetLib.Utils; + +namespace MultiplayerCore.Patches +{ + /// + /// Temporary patch to allow 1.37.0 and 1.37.1 compatibility + /// + [HarmonyPatch] + internal class ForwardCompatHook + { + [HarmonyPostfix] + [HarmonyPatch(typeof(LevelCompletionResults), nameof(LevelCompletionResults.Serialize))] + private static void LevelCompletionResultsSerialize(ref NetDataWriter writer) + { + Plugin.Logger.Debug("LevelCompletionResultsSerialize called"); + try + { + if (UnityGame.GameVersion < new AlmostVersion("1.37.1")) + { + Plugin.Logger.Debug("LevelCompletionResultsSerialize GameVersion is 1.37.0 or lower"); + writer.Put(false); + } + else Plugin.Logger.Debug("LevelCompletionResultsSerialize GameVersion is 1.37.1 or higher, nothing todo"); + } + catch (Exception ex) + { + Plugin.Logger.Warn($"LevelCompletionResultsSerialize Put failed: {ex.Message}"); + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LevelCompletionResults), nameof(INetImmutableSerializable.CreateFromSerializedData))] + private static void LevelCompletionResultsCreateFromSerializedData(ref NetDataReader reader) + { + try + { + if (UnityGame.GameVersion < new AlmostVersion("1.37.1")) + { + Plugin.Logger.Debug("LevelCompletionResultsCreateFromSerializedData GameVersion is 1.37.0 or lower"); + reader.GetBool(); + } + else Plugin.Logger.Debug("LevelCompletionResultsCreateFromSerializedData GameVersion is 1.37.1 or higher, nothing todo"); + } + catch (Exception ex) + { + Plugin.Logger.Warn($"LevelCompletionResultsCreateFromSerializedData GetBool failed, sending player on outdated version?: {ex.Message}"); + } + } + } +} From f54c8112023dfe070041751b7c1133e7a0323998 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 27 Jul 2024 22:16:23 +0200 Subject: [PATCH 41/75] ForwardCompat use simpler method to patch --- MultiplayerCore/Patches/ForwardCompatHook.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiplayerCore/Patches/ForwardCompatHook.cs b/MultiplayerCore/Patches/ForwardCompatHook.cs index 532f260..9117c1a 100644 --- a/MultiplayerCore/Patches/ForwardCompatHook.cs +++ b/MultiplayerCore/Patches/ForwardCompatHook.cs @@ -36,7 +36,7 @@ private static void LevelCompletionResultsSerialize(ref NetDataWriter writer) } [HarmonyPostfix] - [HarmonyPatch(typeof(LevelCompletionResults), nameof(INetImmutableSerializable.CreateFromSerializedData))] + [HarmonyPatch(typeof(LevelCompletionResults), nameof(LevelCompletionResults.CreateFromSerializedData))] private static void LevelCompletionResultsCreateFromSerializedData(ref NetDataReader reader) { try From 08788f8906c9881d6bc687acfbf8a4318b5d305f Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 27 Jul 2024 22:18:35 +0200 Subject: [PATCH 42/75] Move PerPlayerUI into new class MpPerPlayerUI --- MultiplayerCore/Installers/MpMenuInstaller.cs | 10 +- MultiplayerCore/UI/MpDifficultySelector.bsml | 3 + MultiplayerCore/UI/MpPerPlayerToggles.bsml | 11 + MultiplayerCore/UI/MpPerPlayerUI.cs | 360 ++++++++++++++++++ 4 files changed, 375 insertions(+), 9 deletions(-) create mode 100644 MultiplayerCore/UI/MpDifficultySelector.bsml create mode 100644 MultiplayerCore/UI/MpPerPlayerToggles.bsml create mode 100644 MultiplayerCore/UI/MpPerPlayerUI.cs diff --git a/MultiplayerCore/Installers/MpMenuInstaller.cs b/MultiplayerCore/Installers/MpMenuInstaller.cs index 7a5479b..833502a 100644 --- a/MultiplayerCore/Installers/MpMenuInstaller.cs +++ b/MultiplayerCore/Installers/MpMenuInstaller.cs @@ -10,6 +10,7 @@ public override void InstallBindings() { Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); + Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); @@ -18,14 +19,5 @@ public override void InstallBindings() Container.Inject(Container.Resolve()); } - public override void Start() - { - Plugin.Logger?.Info("Installing Interface"); - - LobbySetupViewController lobbySetupViewController = Container.Resolve(); - Container.InstantiateComponent(lobbySetupViewController.gameObject); - - Plugin.Logger?.Info("Installed Interface"); - } } } diff --git a/MultiplayerCore/UI/MpDifficultySelector.bsml b/MultiplayerCore/UI/MpDifficultySelector.bsml new file mode 100644 index 0000000..60500ae --- /dev/null +++ b/MultiplayerCore/UI/MpDifficultySelector.bsml @@ -0,0 +1,3 @@ + + + diff --git a/MultiplayerCore/UI/MpPerPlayerToggles.bsml b/MultiplayerCore/UI/MpPerPlayerToggles.bsml new file mode 100644 index 0000000..5b769cb --- /dev/null +++ b/MultiplayerCore/UI/MpPerPlayerToggles.bsml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs new file mode 100644 index 0000000..32af3ac --- /dev/null +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -0,0 +1,360 @@ +using BeatSaberMarkupLanguage; +using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage.Components.Settings; +using HMUI; +using IPA.Utilities.Async; +using MultiplayerCore.Beatmaps.Providers; +using MultiplayerCore.Networking; +using MultiplayerCore.Players.Packets; +using SiraUtil.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using Zenject; + +namespace MultiplayerCore.UI +{ + internal class MpPerPlayerUI : IInitializable, IDisposable + { + public static string DifficultySelectorResourcePath => "MultiplayerCore.UI.MpDifficultySelector.bsml"; + public static string PerPlayerTogglesResourcePath => "MultiplayerCore.UI.MpPerPlayerToggles.bsml"; + private readonly GameServerLobbyFlowCoordinator _gameServerLobbyFlowCoordinator; + private readonly LobbySetupViewController _lobbyViewController; + private readonly IMultiplayerSessionManager _multiplayerSessionManager; + private readonly ILobbyGameStateController _gameStateController; + private readonly BeatmapLevelsModel _beatmapLevelsModel; + private readonly MpPacketSerializer _packetSerializer; + private readonly MpBeatmapLevelProvider _beatmapLevelProvider; + private BeatmapKey _currentBeatmapKey; + private List? _allowedDiffs; + private CanvasGroup? _difficultyCanvasGroup; + + private readonly SiraLog _logger; + + public MpPerPlayerUI( + GameServerLobbyFlowCoordinator gameServerLobbyFlowCoordinator, + BeatmapLevelsModel beatmapLevelsModel, + IMultiplayerSessionManager sessionManager, + MpBeatmapLevelProvider beatmapLevelProvider, + MpPacketSerializer packetSerializer, + SiraLog logger) + { + _gameServerLobbyFlowCoordinator = gameServerLobbyFlowCoordinator; + _lobbyViewController = _gameServerLobbyFlowCoordinator._lobbySetupViewController; + _gameStateController = _gameServerLobbyFlowCoordinator._lobbyGameStateController; + _beatmapLevelsModel = beatmapLevelsModel; + _multiplayerSessionManager = sessionManager; + _beatmapLevelProvider = beatmapLevelProvider; + _packetSerializer = packetSerializer; + _logger = logger; + } + + // Ignore never assigned warning +#pragma warning disable 0649 // Field is never assigned to, and will always have its default value +#pragma warning disable IDE0044 // Add modifier "readonly" + + [UIComponent("segmentVert")] + private VerticalLayoutGroup? segmentVert; + + [UIComponent("difficultyControl")] + + private TextSegmentedControl? difficultyControl; + + [UIComponent("ppdt")] + private ToggleSetting? ppdt; + + [UIComponent("ppmt")] + private ToggleSetting? ppmt; +#pragma warning restore IDE0044 // Add modifier "readonly" +#pragma warning restore 0649 // Field is never assigned to, and will always have its default value + + public void Initialize() + { + // DifficultySelector + BSMLParser.instance.Parse( + BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), + DifficultySelectorResourcePath), _lobbyViewController._beatmapSelectionView.gameObject, this); + _difficultyCanvasGroup = segmentVert?.gameObject.AddComponent(); + + // PerPlayerToggle + BSMLParser.instance.Parse( + BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), + PerPlayerTogglesResourcePath), _lobbyViewController._startGameReadyButton.gameObject, this); + + // Check UI Elements + if (difficultyControl == null || ppmt == null || ppdt == null || segmentVert == null || _difficultyCanvasGroup == null) + { + _logger.Critical("Error could not initialize UI"); + return; + } + + _lobbyViewController.didActivateEvent += DidActivate; + _lobbyViewController.didDeactivateEvent += DidDeactivate; + + _packetSerializer.RegisterCallback(HandleMpPerPlayerPacket); + _packetSerializer.RegisterType(); + + // We register the callbacks + _gameStateController.lobbyStateChangedEvent += SetLobbyState; + _gameServerLobbyFlowCoordinator._multiplayerLevelSelectionFlowCoordinator.didSelectLevelEvent += LocalSelectedBeatmap; + _gameServerLobbyFlowCoordinator._serverPlayerListViewController.selectSuggestedBeatmapEvent += UpdateDifficultyList; + _lobbyViewController.clearSuggestedBeatmapEvent += ClearLocalSelectedBeatmap; + _gameServerLobbyFlowCoordinator._lobbyPlayerPermissionsModel.permissionsChangedEvent += UpdateButtonsEnabled; + } + + public void Dispose() + { + _lobbyViewController.didActivateEvent -= DidActivate; + _lobbyViewController.didDeactivateEvent -= DidDeactivate; + + _packetSerializer.UnregisterCallback(); + } + + public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) + { + if (firstActivation) + { + var cgubPos = _lobbyViewController._cancelGameUnreadyButton.transform.position; + cgubPos.y -= 0.2f; + _lobbyViewController._cancelGameUnreadyButton.transform.position = cgubPos; + + var sgrbPos = _lobbyViewController._startGameReadyButton.transform.position; + sgrbPos.y -= 0.2f; + _lobbyViewController._startGameReadyButton.transform.position = sgrbPos; + + var csgHHPos = _lobbyViewController._cantStartGameHoverHint.transform.position; + csgHHPos.y -= 0.2f; + _lobbyViewController._cantStartGameHoverHint.transform.position = csgHHPos; + } + else if (!firstActivation) + { + var lobbyPlayersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as LobbyPlayersDataModel; + LobbyPlayerData? playerData = + lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, + out _); + if (playerData != null) + UpdateDifficultyList(playerData.beatmapKey); + + if (addedToHierarchy) + { + // Reset our buttons + ppdt.Value = false; + ppmt.Value = false; + + // Request Updated state + _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); + } + } + + + if (_lobbyViewController._isPartyOwner) + { + ppdt?.gameObject.SetActive(true); + ppmt?.gameObject.SetActive(true); + } + else + { + ppdt?.gameObject.SetActive(false); + ppmt?.gameObject.SetActive(false); + } + } + + public void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) + { + if (removedFromHierarchy) + { + + } + } + + #region DiffListUpdater + public string DiffToStr(BeatmapDifficulty difficulty) + { + return difficulty == BeatmapDifficulty.ExpertPlus ? "Expert+" : difficulty.ToString(); + } + + private void UpdateDifficultyList(BeatmapKey beatmapKey) + { + _currentBeatmapKey = beatmapKey; + if (!_currentBeatmapKey.IsValid()) + { + _logger.Debug("Selected BeatmapKey invalid, disabling difficulty selector"); + segmentVert?.gameObject.SetActive(false); + return; + } + var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); + if (levelHash != null) + { + _logger.Debug($"Level is custom, trying to get beatmap for hash {levelHash}"); + _beatmapLevelProvider.GetBeatmap(levelHash).ContinueWith(levelTask => + { + if (levelTask.IsCompleted && !levelTask.IsFaulted && levelTask.Result != null) + { + var level = levelTask.Result; + _logger.Debug($"Got level {level.LevelHash}, {level.Requirements}, {level.Requirements[beatmapKey.beatmapCharacteristic.serializedName]}"); + // Hacky we use requirements to get the available difficulties + UpdateDifficultyList(level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Keys + .ToList()); + } + }); + } + else + { + _logger.Debug($"LevelId not custom: {beatmapKey.levelId}, getting difficulties from basegame"); + var diffList = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId) + ?.GetDifficulties(beatmapKey.beatmapCharacteristic).ToList(); + if (diffList != null) UpdateDifficultyList(diffList); + } + } + + private void UpdateDifficultyList(IReadOnlyList difficulties) + { + // Ensure that we run on the UnityMainThread here + UnityMainThreadTaskScheduler.Factory.StartNew(() => + { + _allowedDiffs = (from diff in difficulties + where _gameServerLobbyFlowCoordinator._unifiedNetworkPlayerModel.selectionMask.difficulties + .Contains(diff) + select DiffToStr(diff) + ).ToList(); + foreach (var difficultyStr in _allowedDiffs) + _logger.Debug($"Allowed difficulty='{difficultyStr}'"); + + if (_allowedDiffs.Count > 1) + { + segmentVert?.gameObject.SetActive(true); + difficultyControl?.SetTexts(_allowedDiffs); + int index = _allowedDiffs.IndexOf(DiffToStr(_currentBeatmapKey.difficulty)); + if (index > 0) + difficultyControl?.SelectCellWithNumber(index); + } + else + { + _logger.Debug("Only 1 Difficulty available disabling selector"); + segmentVert?.gameObject.SetActive(false); + } + } + ); + } + #endregion + + #region Callbacks + private void HandleMpPerPlayerPacket(MpPerPlayerPacket packet, IConnectedPlayer player) + { + _logger.Debug($"Got MpPerPlayerPacket from {player.userName}|{player.userId} with values PPDEnabled={packet.PPDEnabled}, PPMEnabled={packet.PPMEnabled}"); + if (packet.PPDEnabled != PerPlayerDifficulty || packet.PPMEnabled != PerPlayerModifiers) + { + ppdt.Value = packet.PPDEnabled; + ppmt.Value = packet.PPMEnabled; + } + } + + private void UpdateButtonsEnabled() + { + if (_lobbyViewController._isPartyOwner) + { + ppdt?.gameObject.SetActive(true); + ppmt?.gameObject.SetActive(true); + + // Request updated button states from server + _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); + } + else + { + ppdt?.gameObject.SetActive(false); + ppmt?.gameObject.SetActive(false); + } + } + + private void SetLobbyState(MultiplayerLobbyState lobbyState) + { + _logger.Debug($"Current Lobby State {lobbyState}"); + if (_difficultyCanvasGroup != null) + _difficultyCanvasGroup.alpha = (lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown) ? 1f : 0.25f; + + foreach (var cell in difficultyControl.cells) + { + cell.interactable = lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown; + } + + if (_lobbyViewController == null) + return; + + var raycaster = difficultyControl?.GetComponent(); + + if (_lobbyViewController._isPartyOwner) + { + ppdt.interactable = lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown; + ppmt.interactable = lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown; + } + } + + private void LocalSelectedBeatmap(LevelSelectionFlowCoordinator.State state) + { + _currentBeatmapKey = state.beatmapKey; + UpdateDifficultyList(state.beatmapLevel.GetDifficulties(state.beatmapKey.beatmapCharacteristic).ToList()); + } + + private void ClearLocalSelectedBeatmap() + { + segmentVert?.gameObject.SetActive(false); + _currentBeatmapKey = new BeatmapKey(); + } + + #endregion + #region UIValues + + [UIValue("per-player-diffs")] + public bool PerPlayerDifficulty + { + get => ppdt.Value; + set + { + ppdt.Value = value; + _multiplayerSessionManager.Send(new MpPerPlayerPacket + { + PPDEnabled = ppdt.Value, + PPMEnabled = ppmt.Value + }); + } + } + + [UIValue("per-player-modifiers")] + public bool PerPlayerModifiers + { + get => ppmt.Value; + set + { + ppmt.Value = value; + _multiplayerSessionManager.Send(new MpPerPlayerPacket + { + PPDEnabled = ppdt.Value, + PPMEnabled = ppmt.Value + }); + } + } + + [UIAction("difficulty-selected")] + public void SetSelectedDifficulty(TextSegmentedControl _, int index) + { + var diff = _allowedDiffs[index]; + _logger.Debug($"Selected difficulty at {index} - {diff}"); + if (Enum.TryParse(diff.Replace("Expert+", "ExpertPlus"), out BeatmapDifficulty difficulty)) + { + _currentBeatmapKey = new BeatmapKey(_currentBeatmapKey.levelId, + _currentBeatmapKey.beatmapCharacteristic, difficulty); + _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel.SetLocalPlayerBeatmapLevel(_currentBeatmapKey); + } + } + #endregion + + } +} From fb10d8c41b4e4e6c61e5930475389a252fbd7069 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 3 Aug 2024 18:47:53 +0200 Subject: [PATCH 43/75] Align code and UI xml to better match the Quest port --- MultiplayerCore/UI/MpDifficultySelector.bsml | 2 +- MultiplayerCore/UI/MpPerPlayerToggles.bsml | 4 +-- MultiplayerCore/UI/MpPerPlayerUI.cs | 37 ++++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/MultiplayerCore/UI/MpDifficultySelector.bsml b/MultiplayerCore/UI/MpDifficultySelector.bsml index 60500ae..765180b 100644 --- a/MultiplayerCore/UI/MpDifficultySelector.bsml +++ b/MultiplayerCore/UI/MpDifficultySelector.bsml @@ -1,3 +1,3 @@ - + diff --git a/MultiplayerCore/UI/MpPerPlayerToggles.bsml b/MultiplayerCore/UI/MpPerPlayerToggles.bsml index 5b769cb..9611cb4 100644 --- a/MultiplayerCore/UI/MpPerPlayerToggles.bsml +++ b/MultiplayerCore/UI/MpPerPlayerToggles.bsml @@ -5,7 +5,7 @@ --> - - + + \ No newline at end of file diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs index 32af3ac..a65c63e 100644 --- a/MultiplayerCore/UI/MpPerPlayerUI.cs +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -96,12 +96,12 @@ public void Initialize() _lobbyViewController.didDeactivateEvent += DidDeactivate; _packetSerializer.RegisterCallback(HandleMpPerPlayerPacket); - _packetSerializer.RegisterType(); + _packetSerializer.RegisterCallback(HandleGetMpPerPlayerPacket); // We register the callbacks _gameStateController.lobbyStateChangedEvent += SetLobbyState; _gameServerLobbyFlowCoordinator._multiplayerLevelSelectionFlowCoordinator.didSelectLevelEvent += LocalSelectedBeatmap; - _gameServerLobbyFlowCoordinator._serverPlayerListViewController.selectSuggestedBeatmapEvent += UpdateDifficultyList; + _gameServerLobbyFlowCoordinator._serverPlayerListViewController.selectSuggestedBeatmapEvent += UpdateDifficultyListWithBeatmapKey; _lobbyViewController.clearSuggestedBeatmapEvent += ClearLocalSelectedBeatmap; _gameServerLobbyFlowCoordinator._lobbyPlayerPermissionsModel.permissionsChangedEvent += UpdateButtonsEnabled; } @@ -112,6 +112,7 @@ public void Dispose() _lobbyViewController.didDeactivateEvent -= DidDeactivate; _packetSerializer.UnregisterCallback(); + _packetSerializer.UnregisterCallback(); } public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) @@ -137,7 +138,7 @@ public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screen lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, out _); if (playerData != null) - UpdateDifficultyList(playerData.beatmapKey); + UpdateDifficultyListWithBeatmapKey(playerData.beatmapKey); if (addedToHierarchy) { @@ -177,7 +178,7 @@ public string DiffToStr(BeatmapDifficulty difficulty) return difficulty == BeatmapDifficulty.ExpertPlus ? "Expert+" : difficulty.ToString(); } - private void UpdateDifficultyList(BeatmapKey beatmapKey) + private void UpdateDifficultyListWithBeatmapKey(BeatmapKey beatmapKey) { _currentBeatmapKey = beatmapKey; if (!_currentBeatmapKey.IsValid()) @@ -200,6 +201,7 @@ private void UpdateDifficultyList(BeatmapKey beatmapKey) UpdateDifficultyList(level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Keys .ToList()); } + else _logger.Error($"Failed to get level for hash {levelHash}"); }); } else @@ -234,7 +236,7 @@ select DiffToStr(diff) } else { - _logger.Debug("Only 1 Difficulty available disabling selector"); + _logger.Debug("Less than 2 Difficulties available disabling selector"); segmentVert?.gameObject.SetActive(false); } } @@ -245,12 +247,27 @@ select DiffToStr(diff) #region Callbacks private void HandleMpPerPlayerPacket(MpPerPlayerPacket packet, IConnectedPlayer player) { - _logger.Debug($"Got MpPerPlayerPacket from {player.userName}|{player.userId} with values PPDEnabled={packet.PPDEnabled}, PPMEnabled={packet.PPMEnabled}"); - if (packet.PPDEnabled != PerPlayerDifficulty || packet.PPMEnabled != PerPlayerModifiers) + _logger.Debug($"Received MpPerPlayerPacket from {player.userName}|{player.userId} with values PPDEnabled={packet.PPDEnabled}, PPMEnabled={packet.PPMEnabled}"); + if ((packet.PPDEnabled != PerPlayerDifficulty || packet.PPMEnabled != PerPlayerModifiers) && + ppdt != null && ppmt != null && player.isConnectionOwner) { ppdt.Value = packet.PPDEnabled; ppmt.Value = packet.PPMEnabled; } + else if (!player.isConnectionOwner) + { + _logger.Warn("Player is not Connection Owner, ignoring packet"); + } + } + + private void HandleGetMpPerPlayerPacket(GetMpPerPlayerPacket packet, IConnectedPlayer player) + { + _logger.Debug($"Received GetMpPerPlayerPacket from {player.userName}|{player.userId}"); + // Send MpPerPlayerPacket + var ppPacket = new MpPerPlayerPacket(); + ppPacket.PPDEnabled = ppdt.Value; + ppPacket.PPMEnabled = ppmt.Value; + _multiplayerSessionManager.Send(ppPacket); } private void UpdateButtonsEnabled() @@ -312,7 +329,7 @@ private void ClearLocalSelectedBeatmap() #endregion #region UIValues - [UIValue("per-player-diffs")] + [UIValue("PerPlayerDifficulty")] public bool PerPlayerDifficulty { get => ppdt.Value; @@ -327,7 +344,7 @@ public bool PerPlayerDifficulty } } - [UIValue("per-player-modifiers")] + [UIValue("PerPlayerModifiers")] public bool PerPlayerModifiers { get => ppmt.Value; @@ -342,7 +359,7 @@ public bool PerPlayerModifiers } } - [UIAction("difficulty-selected")] + [UIAction("SetSelectedDifficulty")] public void SetSelectedDifficulty(TextSegmentedControl _, int index) { var diff = _allowedDiffs[index]; From 97a4719fc363b30b2688599ad2afc19b071f6c25 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Thu, 8 Aug 2024 14:36:58 +0200 Subject: [PATCH 44/75] Tweak show toggles disabled, when not party owner, fix toggles dissappearing with ready/start button, update manifest --- MultiplayerCore/MultiplayerCore.csproj | 16 ++++ MultiplayerCore/UI/MpPerPlayerToggles.bsml | 7 +- MultiplayerCore/UI/MpPerPlayerUI.cs | 92 ++++++++++++---------- MultiplayerCore/manifest.json | 2 +- 4 files changed, 69 insertions(+), 48 deletions(-) diff --git a/MultiplayerCore/MultiplayerCore.csproj b/MultiplayerCore/MultiplayerCore.csproj index 86e16dc..6cc1edd 100644 --- a/MultiplayerCore/MultiplayerCore.csproj +++ b/MultiplayerCore/MultiplayerCore.csproj @@ -39,10 +39,17 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + $(BeatSaberDir)\Beat Saber_Data\Managed\AdditionalContentModel.Interfaces.dll False @@ -239,6 +246,15 @@ + + Never + + + Never + + + Never + diff --git a/MultiplayerCore/UI/MpPerPlayerToggles.bsml b/MultiplayerCore/UI/MpPerPlayerToggles.bsml index 9611cb4..5fbba24 100644 --- a/MultiplayerCore/UI/MpPerPlayerToggles.bsml +++ b/MultiplayerCore/UI/MpPerPlayerToggles.bsml @@ -1,9 +1,8 @@ - + - - + + diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs index a65c63e..f0f7102 100644 --- a/MultiplayerCore/UI/MpPerPlayerUI.cs +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -60,6 +60,9 @@ public MpPerPlayerUI( [UIComponent("segmentVert")] private VerticalLayoutGroup? segmentVert; + [UIComponent("ppth")] + private HorizontalLayoutGroup? ppth; + [UIComponent("difficultyControl")] private TextSegmentedControl? difficultyControl; @@ -83,10 +86,10 @@ public void Initialize() // PerPlayerToggle BSMLParser.instance.Parse( BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), - PerPlayerTogglesResourcePath), _lobbyViewController._startGameReadyButton.gameObject, this); + PerPlayerTogglesResourcePath), _lobbyViewController.gameObject, this); // Check UI Elements - if (difficultyControl == null || ppmt == null || ppdt == null || segmentVert == null || _difficultyCanvasGroup == null) + if (difficultyControl == null || ppmt == null || ppdt == null || segmentVert == null || _difficultyCanvasGroup == null || ppth == null) { _logger.Critical("Error could not initialize UI"); return; @@ -143,25 +146,22 @@ public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screen if (addedToHierarchy) { // Reset our buttons - ppdt.Value = false; - ppmt.Value = false; + ppdt!.Value = false; + ppmt!.Value = false; // Request Updated state _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); } } + ppdt!.interactable = _lobbyViewController._isPartyOwner; + ppdt!.text.alpha = _lobbyViewController._isPartyOwner ? 1f : 0.25f; + ppmt!.interactable = _lobbyViewController._isPartyOwner; + ppmt!.text.alpha = _lobbyViewController._isPartyOwner ? 1f : 0.25f; - if (_lobbyViewController._isPartyOwner) - { - ppdt?.gameObject.SetActive(true); - ppmt?.gameObject.SetActive(true); - } - else - { - ppdt?.gameObject.SetActive(false); - ppmt?.gameObject.SetActive(false); - } + // Move toggles to correct position + var locposition = _lobbyViewController._startGameReadyButton.gameObject.transform.localPosition; + ppth!.gameObject.transform.localPosition = locposition; } public void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) @@ -198,8 +198,19 @@ private void UpdateDifficultyListWithBeatmapKey(BeatmapKey beatmapKey) var level = levelTask.Result; _logger.Debug($"Got level {level.LevelHash}, {level.Requirements}, {level.Requirements[beatmapKey.beatmapCharacteristic.serializedName]}"); // Hacky we use requirements to get the available difficulties - UpdateDifficultyList(level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Keys - .ToList()); + if (level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Count > 0) + UpdateDifficultyList(level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Keys + .ToList()); + else + { + _logger.Debug( + $"Level {levelHash} has empty requirements, this should not happen, falling back to packet"); + level = _beatmapLevelProvider.TryGetBeatmapFromPacketHash(levelHash); + if (level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Count > 0) + UpdateDifficultyList(level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Keys + .ToList()); + else _logger.Debug($"Level packet {levelHash} also has empty requirements, this should not happen..."); + } } else _logger.Error($"Failed to get level for hash {levelHash}"); }); @@ -265,52 +276,47 @@ private void HandleGetMpPerPlayerPacket(GetMpPerPlayerPacket packet, IConnectedP _logger.Debug($"Received GetMpPerPlayerPacket from {player.userName}|{player.userId}"); // Send MpPerPlayerPacket var ppPacket = new MpPerPlayerPacket(); - ppPacket.PPDEnabled = ppdt.Value; - ppPacket.PPMEnabled = ppmt.Value; + ppPacket.PPDEnabled = ppdt!.Value; + ppPacket.PPMEnabled = ppmt!.Value; _multiplayerSessionManager.Send(ppPacket); } private void UpdateButtonsEnabled() { - if (_lobbyViewController._isPartyOwner) - { - ppdt?.gameObject.SetActive(true); - ppmt?.gameObject.SetActive(true); + ppdt!.interactable = _lobbyViewController._isPartyOwner; + ppdt!.text.alpha = _lobbyViewController._isPartyOwner ? 1f : 0.25f; + ppmt!.interactable = _lobbyViewController._isPartyOwner; + ppmt!.text.alpha = _lobbyViewController._isPartyOwner ? 1f : 0.25f; - // Request updated button states from server - _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); - } - else - { - ppdt?.gameObject.SetActive(false); - ppmt?.gameObject.SetActive(false); - } + // Request updated button states from server + _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); } private void SetLobbyState(MultiplayerLobbyState lobbyState) { _logger.Debug($"Current Lobby State {lobbyState}"); - if (_difficultyCanvasGroup != null) - _difficultyCanvasGroup.alpha = (lobbyState == MultiplayerLobbyState.LobbySetup || - lobbyState == MultiplayerLobbyState.LobbyCountdown) ? 1f : 0.25f; - foreach (var cell in difficultyControl.cells) + _difficultyCanvasGroup!.alpha = (lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown) ? 1f : 0.25f; + + foreach (var cell in difficultyControl!.cells) { cell.interactable = lobbyState == MultiplayerLobbyState.LobbySetup || lobbyState == MultiplayerLobbyState.LobbyCountdown; } - if (_lobbyViewController == null) - return; - - var raycaster = difficultyControl?.GetComponent(); - if (_lobbyViewController._isPartyOwner) { - ppdt.interactable = lobbyState == MultiplayerLobbyState.LobbySetup || - lobbyState == MultiplayerLobbyState.LobbyCountdown; - ppmt.interactable = lobbyState == MultiplayerLobbyState.LobbySetup || - lobbyState == MultiplayerLobbyState.LobbyCountdown; + ppdt!.interactable = lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown; + ppmt!.interactable = lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown; + + ppdt!.text.alpha = (lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown) ? 1f : 0.25f; + + ppmt!.text.alpha = (lobbyState == MultiplayerLobbyState.LobbySetup || + lobbyState == MultiplayerLobbyState.LobbyCountdown) ? 1f : 0.25f; } } diff --git a/MultiplayerCore/manifest.json b/MultiplayerCore/manifest.json index dc67d50..8081eee 100644 --- a/MultiplayerCore/manifest.json +++ b/MultiplayerCore/manifest.json @@ -5,7 +5,7 @@ "author": "Goobwabber", "version": "1.5.0", "description": "Adds custom songs to Beat Saber Multiplayer.", - "gameVersion": "1.35.0", + "gameVersion": "1.37.2", "dependsOn": { "BSIPA": "^4.3.3", "SongCore": "^3.13.0", From 030ae5f9ce34ee7ffabf7668340207e89d5b8a77 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 14 Aug 2024 21:20:25 +0200 Subject: [PATCH 45/75] Ensure newly joined players receive map selection --- MultiplayerCore/Objects/MpPlayersDataModel.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index 96d4c94..758904b 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -37,6 +37,7 @@ internal MpPlayersDataModel( _menuRpcManager.getRecommendedBeatmapEvent += this.HandleMenuRpcManagerGetRecommendedBeatmap; _menuRpcManager.recommendBeatmapEvent -= base.HandleMenuRpcManagerRecommendBeatmap; _menuRpcManager.recommendBeatmapEvent += this.HandleMenuRpcManagerRecommendBeatmap; + _multiplayerSessionManager.playerConnectedEvent += HandlePlayerConnected; } public new void Deactivate() @@ -46,13 +47,20 @@ internal MpPlayersDataModel( _menuRpcManager.getRecommendedBeatmapEvent += base.HandleMenuRpcManagerGetRecommendedBeatmap; _menuRpcManager.recommendBeatmapEvent -= this.HandleMenuRpcManagerRecommendBeatmap; _menuRpcManager.recommendBeatmapEvent += base.HandleMenuRpcManagerRecommendBeatmap; - base.Deactivate(); + _multiplayerSessionManager.playerConnectedEvent -= HandlePlayerConnected; + base.Deactivate(); } public new void Dispose() => Deactivate(); - internal void SetLocalPlayerBeatmapLevel_override(in BeatmapKey beatmapKey) + internal void HandlePlayerConnected(IConnectedPlayer connectedPlayer) + { + // Send our MpBeatmapPacket again so they have it + var selectedBeatmapKey = _playersData[localUserId].beatmapKey; + SendMpBeatmapPacket(selectedBeatmapKey); + } + internal void SetLocalPlayerBeatmapLevel_override(in BeatmapKey beatmapKey) { // Game: The local player has selected / recommended a beatmap From f55582bc3826259b6c64254421bc007e3bac40de Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 14 Aug 2024 21:21:39 +0200 Subject: [PATCH 46/75] Fix missing used function in MpPerPlayerUI --- .../Beatmaps/Providers/MpBeatmapLevelProvider.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs index 72fef57..fff0e76 100644 --- a/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs +++ b/MultiplayerCore/Beatmaps/Providers/MpBeatmapLevelProvider.cs @@ -77,5 +77,16 @@ public MpBeatmap GetBeatmapFromPacket(MpBeatmapPacket packet) _hashToNetworkMaps.Add(packet.levelHash, map); return map; } - } + + /// + /// Gets an from the information in the provided packet. + /// + /// The hash of the packet we want + /// An with a cover from BeatSaver. + public MpBeatmap? TryGetBeatmapFromPacketHash(string hash) + { + if (_hashToNetworkMaps.TryGetValue(hash, out var map)) return map; + return null; + } + } } From 764c7a5ee72be35969cca64da54404e0c51396c1 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 14 Aug 2024 21:22:06 +0200 Subject: [PATCH 47/75] Fix toggles not enabling when becoming lobby host --- MultiplayerCore/UI/MpPerPlayerUI.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs index f0f7102..68dd45d 100644 --- a/MultiplayerCore/UI/MpPerPlayerUI.cs +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -283,10 +283,11 @@ private void HandleGetMpPerPlayerPacket(GetMpPerPlayerPacket packet, IConnectedP private void UpdateButtonsEnabled() { - ppdt!.interactable = _lobbyViewController._isPartyOwner; - ppdt!.text.alpha = _lobbyViewController._isPartyOwner ? 1f : 0.25f; - ppmt!.interactable = _lobbyViewController._isPartyOwner; - ppmt!.text.alpha = _lobbyViewController._isPartyOwner ? 1f : 0.25f; + bool isPartyOwner = _gameServerLobbyFlowCoordinator._lobbyPlayerPermissionsModel.isPartyOwner; + ppdt!.interactable = isPartyOwner; + ppdt!.text.alpha = isPartyOwner ? 1f : 0.25f; + ppmt!.interactable = isPartyOwner; + ppmt!.text.alpha = isPartyOwner ? 1f : 0.25f; // Request updated button states from server _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); From 503c1080ba394f07eaf19a22c89d2934ce370470 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 14 Aug 2024 21:30:24 +0200 Subject: [PATCH 48/75] Fix ScoreSyncState Packet --- MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs b/MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs index 1e1320c..f49c956 100644 --- a/MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs +++ b/MultiplayerCore/ScoreSyncState/MpScoreSyncStatePacket.cs @@ -9,8 +9,8 @@ internal class MpScoreSyncStatePacket : MpPacket public long fullStateUpdateFrequency = 500L; public override void Serialize(NetDataWriter writer) { - writer.Put(deltaUpdateFrequency); - writer.Put(fullStateUpdateFrequency); + writer.PutVarLong(deltaUpdateFrequency); + writer.PutVarLong(fullStateUpdateFrequency); } public override void Deserialize(NetDataReader reader) From c42ef3ccd2f177a0e2f4f356604192d41c64a0d1 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 14 Aug 2024 22:11:48 +0200 Subject: [PATCH 49/75] Don't reference types not available in older game versions --- MultiplayerCore/Objects/MpLevelLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index d447a42..a1cbca2 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -37,7 +37,7 @@ internal MpLevelLoader( [UsedImplicitly] public void LoadLevel_override(string levelId) { - var levelHash = Utilities.HashForLevelID(levelId); + var levelHash = Utilities.HashForLevelID(levelId); if (levelHash == null) { @@ -120,7 +120,7 @@ private async Task DownloadBeatmapLevelAsync(string { if (method.GetParameters().Length > 2) loadResult = await (Task)method.Invoke(_beatmapLevelsModel, - new object[] { levelId, BeatmapLevelDataVersion.Original, cancellationToken }); + new object[] { levelId, 0, cancellationToken }); else if (method.GetParameters().Length == 2) loadResult = await (Task)method.Invoke(_beatmapLevelsModel, new object[] { levelId, cancellationToken }); From 4dcccadd8d0584b252f78248bd171c0a5b622b02 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Thu, 15 Aug 2024 21:03:55 +0200 Subject: [PATCH 50/75] Fallback to beatsaver data when MpBeatmapPacket is missing --- MultiplayerCore/Objects/MpPlayersDataModel.cs | 7 +- .../Patchers/BeatmapSelectionViewPatcher.cs | 86 +++++++----- .../GameServerPlayerTableCellPatcher.cs | 131 ++++++++++-------- 3 files changed, 133 insertions(+), 91 deletions(-) diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index 758904b..281f28b 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -56,7 +56,7 @@ internal MpPlayersDataModel( internal void HandlePlayerConnected(IConnectedPlayer connectedPlayer) { - // Send our MpBeatmapPacket again so they have it + // Send our MpBeatmapPacket again so newly joined players have it var selectedBeatmapKey = _playersData[localUserId].beatmapKey; SendMpBeatmapPacket(selectedBeatmapKey); } @@ -96,8 +96,9 @@ private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer private new void HandleMenuRpcManagerRecommendBeatmap(string userId, BeatmapKeyNetSerializable beatmapKeySerializable) { // RPC: Another player has recommended a beatmap (base game) - - if (!string.IsNullOrEmpty(Utilities.HashForLevelID(beatmapKeySerializable.levelID))) + + var levelHash = Utilities.HashForLevelID(beatmapKeySerializable.levelID); + if (!string.IsNullOrEmpty(levelHash) && _beatmapLevelProvider.TryGetBeatmapFromPacketHash(levelHash!) != null) // If we have no packet run basegame behaviour return; base.HandleMenuRpcManagerRecommendBeatmap(userId, beatmapKeySerializable); diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs index 74cd0c4..8334f2d 100644 --- a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -4,8 +4,12 @@ using MultiplayerCore.Objects; using SiraUtil.Affinity; using System; +using System.Collections; using System.Collections.Generic; using System.Reflection; +using System.Threading.Tasks; +using MultiplayerCore.Beatmaps.Packets; +using UnityEngine; namespace MultiplayerCore.Patchers { @@ -37,7 +41,7 @@ internal class BeatmapSelectionViewPatcher : IAffinity [AffinityPrefix] [AffinityPatch(typeof(EditableBeatmapSelectionView), nameof(EditableBeatmapSelectionView.SetBeatmap))] - public bool EditableBeatmapSelectionView_SetBeatmap(ref EditableBeatmapSelectionView __instance, in BeatmapKey beatmapKey) + public bool EditableBeatmapSelectionView_SetBeatmap(EditableBeatmapSelectionView __instance, in BeatmapKey beatmapKey) { if (_mpPlayersDataModel == null) return false; if (!beatmapKey.IsValid()) return true; @@ -47,23 +51,8 @@ public bool EditableBeatmapSelectionView_SetBeatmap(ref EditableBeatmapSelection if (string.IsNullOrWhiteSpace(levelHash)) return true; var packet = _mpPlayersDataModel.FindLevelPacket(levelHash!); - if (packet == null) return true; - __instance._clearButton.gameObject.SetActive(__instance.showClearButton); - __instance._noLevelText.enabled = false; - __instance._levelBar.hide = false; - - var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); - - Plugin.Logger.Debug($"Calling Setup/SetupData with level type: {level.GetType().Name}, beatmapCharacteristic type: {beatmapKey.beatmapCharacteristic.GetType().Name}, difficulty type: {beatmapKey.difficulty.GetType().Name} "); - if (_newlbarInfo) - { - _lbarInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); - } - else - { - _lbarInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); - } + __instance.StartCoroutine(SetBeatmapCoroutine(__instance, beatmapKey, levelHash!, packet)); return false; } @@ -79,25 +68,54 @@ public bool BeatmapSelectionView_SetBeatmap(ref BeatmapSelectionView __instance, if (string.IsNullOrWhiteSpace(levelHash)) return true; var packet = _mpPlayersDataModel.FindLevelPacket(levelHash!); - if (packet == null) return true; - - __instance._noLevelText.enabled = false; - __instance._levelBar.hide = false; - - var level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(beatmapKey, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); - - Plugin.Logger.Debug($"Calling Setup/SetupData with level type: {level.GetType().Name}, beatmapCharacteristic type: {beatmapKey.beatmapCharacteristic.GetType().Name}, difficulty type: {beatmapKey.difficulty.GetType().Name} "); - if (_newlbarInfo) - { - _lbarInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); - } - else - { - _lbarInfo.Invoke(__instance._levelBar, new object[] { level, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); - } + + __instance.StartCoroutine(SetBeatmapCoroutine(__instance, beatmapKey, levelHash!, packet)); return false; } - } + + IEnumerator SetBeatmapCoroutine(BeatmapSelectionView instance, BeatmapKey key, string levelHash, MpBeatmapPacket? packet = null) + { + BeatmapLevel? level; + if (packet != null) + { + level = _mpBeatmapLevelProvider.GetBeatmapFromPacket(packet).MakeBeatmapLevel(key, + _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(packet.levelHash)); + } + else + { + var levelTask = _mpBeatmapLevelProvider.GetBeatmap(levelHash!); + yield return new WaitUntil(() => levelTask.IsCompleted); + + level = levelTask.Result?.MakeBeatmapLevel(key, + _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(levelHash!)); + } + + if (level != null) + { + if (instance is EditableBeatmapSelectionView editView) editView._clearButton.gameObject.SetActive(editView.showClearButton); + instance._noLevelText.enabled = false; + instance._levelBar.hide = false; + + + Plugin.Logger.Debug($"Calling Setup/SetupData with level type: {level.GetType().Name}, beatmapCharacteristic type: {key.beatmapCharacteristic.GetType().Name}, difficulty type: {key.difficulty.GetType().Name} "); + if (_newlbarInfo) + { + _lbarInfo.Invoke(instance._levelBar, new object[] { level, key.difficulty, key.beatmapCharacteristic }); + } + else + { + _lbarInfo.Invoke(instance._levelBar, new object[] { level, key.beatmapCharacteristic, key.difficulty }); + } + } + else + { + Plugin.Logger.Error($"Could not get level info for level {levelHash}"); + if (instance is EditableBeatmapSelectionView editView) editView._clearButton.gameObject.SetActive(false); + instance._noLevelText.enabled = true; + instance._levelBar.hide = true; + } + } + } internal static class PacketExt { diff --git a/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs b/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs index a01bac1..0b91a12 100644 --- a/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs +++ b/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs @@ -1,22 +1,25 @@ using SiraUtil.Affinity; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections; using System.Threading.Tasks; -using MultiplayerCore.UI; using MultiplayerCore.Objects; -using Zenject; -using System.Diagnostics.Eventing.Reader; using BGLib.Polyglot; +using MultiplayerCore.Beatmaps.Abstractions; +using MultiplayerCore.Beatmaps.Providers; namespace MultiplayerCore.Patchers { internal class GameServerPlayerTableCellPatcher : IAffinity { - private MpPlayersDataModel _mpPlayersDataModel; + private MpPlayersDataModel? _mpPlayersDataModel; + private MpBeatmapLevelProvider _mpBeatmapLevelProvider; + private ICoroutineStarter _sharedCoroutineStarter; - GameServerPlayerTableCellPatcher(ILobbyPlayersDataModel playersDataModel) => _mpPlayersDataModel = playersDataModel as MpPlayersDataModel; + GameServerPlayerTableCellPatcher(ILobbyPlayersDataModel playersDataModel, MpBeatmapLevelProvider mpBeatmapLevelProvider, ICoroutineStarter sharedCoroutineStarter) + { + _mpPlayersDataModel = playersDataModel as MpPlayersDataModel; + _mpBeatmapLevelProvider = mpBeatmapLevelProvider; + _sharedCoroutineStarter = sharedCoroutineStarter; + } [AffinityPrefix] [AffinityPatch(typeof(GameServerPlayerTableCell), nameof(GameServerPlayerTableCell.SetData))] @@ -38,58 +41,78 @@ bool GameServerPlayerTableCell_SetData(ref GameServerPlayerTableCell __instance, else statusView.sprite = __instance._spectatingIcon; } - var key = playerData.beatmapKey; - bool validKey = key.IsValid(); - bool displayLevelText = validKey; - if (validKey) - { - var level = __instance._beatmapLevelsModel.GetBeatmapLevel(key.levelId); - var levelHash = Utilities.HashForLevelID(key.levelId); - __instance._suggestedLevelText.text = level?.songName; - displayLevelText = level != null; + var key = playerData.beatmapKey; + //if (!key.IsValid()) return true; + _sharedCoroutineStarter.StartCoroutine(SetDataCoroutine(__instance, connectedPlayer, playerData, key, hasKickPermissions, + allowSelection, getLevelEntitlementTask)); + return false; + } + IEnumerator SetDataCoroutine(GameServerPlayerTableCell instance, IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData, BeatmapKey key, bool hasKickPermissions, bool allowSelection, Task? getLevelEntitlementTask) + { + Plugin.Logger.Debug($"Start SetDataCoroutine with key {key.levelId} diff {key.difficulty.Name()}"); + bool displayLevelText = key.IsValid(); + if (displayLevelText) + { + Plugin.Logger.Debug("displayLevelText if check start"); + var level = instance._beatmapLevelsModel.GetBeatmapLevel(key.levelId); + var levelHash = Utilities.HashForLevelID(key.levelId); + instance._suggestedLevelText.text = level?.songName; + displayLevelText = level != null; - if (level == null && _mpPlayersDataModel != null && !string.IsNullOrEmpty(levelHash)) // we didn't have the level, but we can attempt to get the packet - { - var packet = _mpPlayersDataModel.FindLevelPacket(levelHash); - __instance._suggestedLevelText.text = packet?.songName; - displayLevelText = packet != null; - } + if (level == null && _mpPlayersDataModel != null && !string.IsNullOrEmpty(levelHash)) // we didn't have the level, but we can attempt to get the packet + { + Plugin.Logger.Debug("FindLevelPacket running"); + var packet = _mpPlayersDataModel.FindLevelPacket(levelHash); + instance._suggestedLevelText.text = packet?.songName; - __instance._suggestedCharacteristicIcon.sprite = key.beatmapCharacteristic.icon; - __instance._suggestedDifficultyText.text = key.difficulty.ShortName(); - } - SetLevelFoundValues(__instance, displayLevelText); - bool anyModifiers = !(playerData?.gameplayModifiers?.IsWithoutModifiers() ?? true); - __instance._suggestedModifiersList.gameObject.SetActive(anyModifiers); - __instance._emptySuggestedModifiersText.gameObject.SetActive(!anyModifiers); + Task? mpLevelTask = null; + if (packet == null) + { + Plugin.Logger.Debug("Could not find packet, trying beatsaver"); + mpLevelTask = _mpBeatmapLevelProvider.GetBeatmap(levelHash); + yield return IPA.Utilities.Async.Coroutines.WaitForTask(mpLevelTask); + Plugin.Logger.Debug($"Task finished SongName={mpLevelTask.Result?.SongName}"); + instance._suggestedLevelText.text = mpLevelTask.Result?.SongName; + } - if (anyModifiers) - { - var modifiers = __instance._gameplayModifiers.CreateModifierParamsList(playerData.gameplayModifiers); - __instance._emptySuggestedModifiersText.gameObject.SetActive(modifiers.Count == 0); - if (modifiers.Count > 0) - { - __instance._suggestedModifiersList.SetData(modifiers.Count, (int id, GameplayModifierInfoListItem listItem) => listItem.SetModifier(modifiers[id], false)); - } - } + displayLevelText = packet != null || mpLevelTask?.Result != null; + Plugin.Logger.Debug($"Will display level text? {displayLevelText}"); + } - __instance._useModifiersButton.interactable = !connectedPlayer.isMe && anyModifiers && allowSelection; - __instance._kickPlayerButton.interactable = !connectedPlayer.isMe && hasKickPermissions && allowSelection; - __instance._mutePlayerButton.gameObject.SetActive(false); - if (getLevelEntitlementTask != null && !connectedPlayer.isMe) - { - __instance._useBeatmapButtonHoverHint.text = Localization.Get("LABEL_CANT_START_GAME_DO_NOT_OWN_SONG"); - __instance.SetBeatmapUseButtonEnabledAsync(getLevelEntitlementTask); - return false; - } + instance._suggestedCharacteristicIcon.sprite = key.beatmapCharacteristic.icon; + instance._suggestedDifficultyText.text = key.difficulty.ShortName(); + } else Plugin.Logger.Debug("Player key was invalid"); + SetLevelFoundValues(instance, displayLevelText); + bool anyModifiers = !(playerData?.gameplayModifiers?.IsWithoutModifiers() ?? true); + instance._suggestedModifiersList.gameObject.SetActive(anyModifiers); + instance._emptySuggestedModifiersText.gameObject.SetActive(!anyModifiers); - __instance._useBeatmapButton.interactable = false; - __instance._useBeatmapButtonHoverHint.enabled = false; + if (anyModifiers) + { + var modifiers = instance._gameplayModifiers.CreateModifierParamsList(playerData.gameplayModifiers); + instance._emptySuggestedModifiersText.gameObject.SetActive(modifiers.Count == 0); + if (modifiers.Count > 0) + { + instance._suggestedModifiersList.SetData(modifiers.Count, (int id, GameplayModifierInfoListItem listItem) => listItem.SetModifier(modifiers[id], false)); + } + } - return false; - } + instance._useModifiersButton.interactable = !connectedPlayer.isMe && anyModifiers && allowSelection; + instance._kickPlayerButton.interactable = !connectedPlayer.isMe && hasKickPermissions && allowSelection; + instance._mutePlayerButton.gameObject.SetActive(false); + if (getLevelEntitlementTask != null && !connectedPlayer.isMe) + { + instance._useBeatmapButtonHoverHint.text = Localization.Get("LABEL_CANT_START_GAME_DO_NOT_OWN_SONG"); + instance.SetBeatmapUseButtonEnabledAsync(getLevelEntitlementTask); + yield break; + } + + instance._useBeatmapButton.interactable = false; + instance._useBeatmapButtonHoverHint.enabled = false; + + } - void SetLevelFoundValues(GameServerPlayerTableCell __instance, bool displayLevelText) + void SetLevelFoundValues(GameServerPlayerTableCell __instance, bool displayLevelText) { __instance._suggestedLevelText.gameObject.SetActive(displayLevelText); __instance._suggestedCharacteristicIcon.gameObject.SetActive(displayLevelText); From 1ff6c84adb4854f893e672990d38093e09b731f5 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Thu, 15 Aug 2024 21:05:00 +0200 Subject: [PATCH 51/75] Comment unused code, attempt move toggles when partyhost changes (not working) --- MultiplayerCore/UI/MpPerPlayerUI.cs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs index 68dd45d..295cd3c 100644 --- a/MultiplayerCore/UI/MpPerPlayerUI.cs +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -96,7 +96,7 @@ public void Initialize() } _lobbyViewController.didActivateEvent += DidActivate; - _lobbyViewController.didDeactivateEvent += DidDeactivate; + //_lobbyViewController.didDeactivateEvent += DidDeactivate; _packetSerializer.RegisterCallback(HandleMpPerPlayerPacket); _packetSerializer.RegisterCallback(HandleGetMpPerPlayerPacket); @@ -112,7 +112,7 @@ public void Initialize() public void Dispose() { _lobbyViewController.didActivateEvent -= DidActivate; - _lobbyViewController.didDeactivateEvent -= DidDeactivate; + //_lobbyViewController.didDeactivateEvent -= DidDeactivate; _packetSerializer.UnregisterCallback(); _packetSerializer.UnregisterCallback(); @@ -164,15 +164,16 @@ public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screen ppth!.gameObject.transform.localPosition = locposition; } - public void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) - { - if (removedFromHierarchy) - { + //public void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) + //{ + // if (removedFromHierarchy) + // { - } - } + // } + //} #region DiffListUpdater + // TODO: Possibly replace with BeatmapDifficultyMethods.Name ext method see BeatmapDifficultySegmentedControlController public string DiffToStr(BeatmapDifficulty difficulty) { return difficulty == BeatmapDifficulty.ExpertPlus ? "Expert+" : difficulty.ToString(); @@ -289,6 +290,10 @@ private void UpdateButtonsEnabled() ppmt!.interactable = isPartyOwner; ppmt!.text.alpha = isPartyOwner ? 1f : 0.25f; + // Move toggles to correct position + var locposition = _lobbyViewController._startGameReadyButton.gameObject.transform.localPosition; + ppth!.gameObject.transform.localPosition = locposition; + // Request updated button states from server _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); } From 246dd1980aff197f76f1c993c54cb755b918ae93 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Fri, 16 Aug 2024 19:12:57 +0200 Subject: [PATCH 52/75] Use IPA WaitForTask instead of a WaitUntil, remove unused using statements --- MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs index 8334f2d..b122aa0 100644 --- a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -7,9 +7,7 @@ using System.Collections; using System.Collections.Generic; using System.Reflection; -using System.Threading.Tasks; using MultiplayerCore.Beatmaps.Packets; -using UnityEngine; namespace MultiplayerCore.Patchers { @@ -84,7 +82,7 @@ IEnumerator SetBeatmapCoroutine(BeatmapSelectionView instance, BeatmapKey key, s else { var levelTask = _mpBeatmapLevelProvider.GetBeatmap(levelHash!); - yield return new WaitUntil(() => levelTask.IsCompleted); + yield return IPA.Utilities.Async.Coroutines.WaitForTask(levelTask); level = levelTask.Result?.MakeBeatmapLevel(key, _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(levelHash!)); From 547f49ff5d809e38c1f90da03ecc3004837993b4 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Fri, 16 Aug 2024 19:13:49 +0200 Subject: [PATCH 53/75] Cleanup and improve code --- .../Patchers/GameServerPlayerTableCellPatcher.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs b/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs index 0b91a12..552aeff 100644 --- a/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs +++ b/MultiplayerCore/Patchers/GameServerPlayerTableCellPatcher.cs @@ -41,19 +41,17 @@ bool GameServerPlayerTableCell_SetData(ref GameServerPlayerTableCell __instance, else statusView.sprite = __instance._spectatingIcon; } - var key = playerData.beatmapKey; - //if (!key.IsValid()) return true; - _sharedCoroutineStarter.StartCoroutine(SetDataCoroutine(__instance, connectedPlayer, playerData, key, hasKickPermissions, + _sharedCoroutineStarter.StartCoroutine(SetDataCoroutine(__instance, connectedPlayer, playerData, hasKickPermissions, allowSelection, getLevelEntitlementTask)); return false; } - IEnumerator SetDataCoroutine(GameServerPlayerTableCell instance, IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData, BeatmapKey key, bool hasKickPermissions, bool allowSelection, Task? getLevelEntitlementTask) + IEnumerator SetDataCoroutine(GameServerPlayerTableCell instance, IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData, bool hasKickPermissions, bool allowSelection, Task? getLevelEntitlementTask) { + var key = playerData.beatmapKey; Plugin.Logger.Debug($"Start SetDataCoroutine with key {key.levelId} diff {key.difficulty.Name()}"); bool displayLevelText = key.IsValid(); if (displayLevelText) { - Plugin.Logger.Debug("displayLevelText if check start"); var level = instance._beatmapLevelsModel.GetBeatmapLevel(key.levelId); var levelHash = Utilities.HashForLevelID(key.levelId); instance._suggestedLevelText.text = level?.songName; @@ -61,7 +59,6 @@ IEnumerator SetDataCoroutine(GameServerPlayerTableCell instance, IConnectedPlaye if (level == null && _mpPlayersDataModel != null && !string.IsNullOrEmpty(levelHash)) // we didn't have the level, but we can attempt to get the packet { - Plugin.Logger.Debug("FindLevelPacket running"); var packet = _mpPlayersDataModel.FindLevelPacket(levelHash); instance._suggestedLevelText.text = packet?.songName; @@ -69,7 +66,7 @@ IEnumerator SetDataCoroutine(GameServerPlayerTableCell instance, IConnectedPlaye if (packet == null) { Plugin.Logger.Debug("Could not find packet, trying beatsaver"); - mpLevelTask = _mpBeatmapLevelProvider.GetBeatmap(levelHash); + mpLevelTask = _mpBeatmapLevelProvider.GetBeatmapFromBeatSaver(levelHash); yield return IPA.Utilities.Async.Coroutines.WaitForTask(mpLevelTask); Plugin.Logger.Debug($"Task finished SongName={mpLevelTask.Result?.SongName}"); instance._suggestedLevelText.text = mpLevelTask.Result?.SongName; @@ -81,7 +78,7 @@ IEnumerator SetDataCoroutine(GameServerPlayerTableCell instance, IConnectedPlaye instance._suggestedCharacteristicIcon.sprite = key.beatmapCharacteristic.icon; instance._suggestedDifficultyText.text = key.difficulty.ShortName(); - } else Plugin.Logger.Debug("Player key was invalid"); + } else Plugin.Logger.Debug("Player key was not valid, disabling"); SetLevelFoundValues(instance, displayLevelText); bool anyModifiers = !(playerData?.gameplayModifiers?.IsWithoutModifiers() ?? true); instance._suggestedModifiersList.gameObject.SetActive(anyModifiers); From e40b45d21b6809ca6ab5aa075ae5f355b5c19a0b Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 17 Aug 2024 20:13:09 +0200 Subject: [PATCH 54/75] Fix blackscreen when map can't be downloaded / download fails --- MultiplayerCore/Objects/MpLevelLoader.cs | 9 ++- MultiplayerCore/Objects/MpPlayersDataModel.cs | 8 +- .../Patchers/BeatmapSelectionViewPatcher.cs | 4 +- .../Patches/NoLevelSpectatorPatch.cs | 81 +++++++++++++++++++ 4 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 MultiplayerCore/Patches/NoLevelSpectatorPatch.cs diff --git a/MultiplayerCore/Objects/MpLevelLoader.cs b/MultiplayerCore/Objects/MpLevelLoader.cs index a1cbca2..885d757 100644 --- a/MultiplayerCore/Objects/MpLevelLoader.cs +++ b/MultiplayerCore/Objects/MpLevelLoader.cs @@ -104,7 +104,12 @@ private async Task DownloadBeatmapLevelAsync(string // Download from BeatSaver var success = await _levelDownloader.TryDownloadLevel(levelId, cancellationToken, this); if (!success) - throw new Exception($"Failed to download level: {levelId}"); + { + // If the download fails we go into spectator + _rpcManager.SetIsEntitledToLevel(levelId, EntitlementsStatus.NotOwned); + _beatmapLevelData = null; + return LoadBeatmapLevelDataResult.Error; + } // Reload custom level set _logger.Debug("Reloading custom level collection..."); @@ -127,7 +132,7 @@ private async Task DownloadBeatmapLevelAsync(string else throw new NotSupportedException("Game version not supported"); } //var loadResult = await _beatmapLevelsModel.LoadBeatmapLevelDataAsync(levelId, BeatmapLevelDataVersion.Original, cancellationToken); - if (loadResult.isError) + if (loadResult.isError) _logger.Error($"Custom level data could not be loaded after download: {levelId}"); return loadResult; } diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index 281f28b..f969062 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -14,7 +14,7 @@ namespace MultiplayerCore.Objects internal class MpPlayersDataModel : LobbyPlayersDataModel, ILobbyPlayersDataModel, IDisposable { private readonly MpPacketSerializer _packetSerializer; - private readonly MpBeatmapLevelProvider _beatmapLevelProvider; + internal readonly MpBeatmapLevelProvider _beatmapLevelProvider; private readonly SiraLog _logger; private readonly Dictionary _lastPlayerBeatmapPackets = new(); public IReadOnlyDictionary PlayerPackets => _lastPlayerBeatmapPackets; @@ -57,9 +57,9 @@ internal MpPlayersDataModel( internal void HandlePlayerConnected(IConnectedPlayer connectedPlayer) { // Send our MpBeatmapPacket again so newly joined players have it - var selectedBeatmapKey = _playersData[localUserId].beatmapKey; - SendMpBeatmapPacket(selectedBeatmapKey); - } + var selectedBeatmapKey = _playersData[localUserId].beatmapKey; + SendMpBeatmapPacket(selectedBeatmapKey); + } internal void SetLocalPlayerBeatmapLevel_override(in BeatmapKey beatmapKey) { // Game: The local player has selected / recommended a beatmap diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs index b122aa0..87d996c 100644 --- a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -112,10 +112,10 @@ IEnumerator SetBeatmapCoroutine(BeatmapSelectionView instance, BeatmapKey key, s instance._noLevelText.enabled = true; instance._levelBar.hide = true; } - } + } } - internal static class PacketExt + public static class PacketExt { public static BeatmapLevel MakeBeatmapLevel(this MpBeatmap mpBeatmap, in BeatmapKey key, IPreviewMediaData previewMediaData) { diff --git a/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs b/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs new file mode 100644 index 0000000..b138f76 --- /dev/null +++ b/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BGLib.Polyglot; +using HarmonyLib; +using MultiplayerCore.Beatmaps; +using MultiplayerCore.Beatmaps.Providers; +using MultiplayerCore.Objects; +using MultiplayerCore.Patchers; +using Zenject; + +namespace MultiplayerCore.Patches +{ + [HarmonyPatch] + internal class NoLevelSpectatorPatch + { + private static MpBeatmapLevelProvider? mpBeatmapLevelProvider; + + [HarmonyPrefix] + [HarmonyPatch(typeof(LobbyGameStateController), nameof(LobbyGameStateController.StartMultiplayerLevel))] + internal static bool LobbyGameStateController_StartMultiplayerLevel(LobbyGameStateController __instance, ILevelGameplaySetupData gameplaySetupData, IBeatmapLevelData beatmapLevelData, Action beforeSceneSwitchCallback) + { + mpBeatmapLevelProvider = ((MpPlayersDataModel)__instance._lobbyPlayersDataModel)._beatmapLevelProvider; + + var levelHash = Utilities.HashForLevelID(gameplaySetupData.beatmapKey.levelId); + if (gameplaySetupData != null && beatmapLevelData == null && !string.IsNullOrWhiteSpace(levelHash)) + { + Plugin.Logger.Info($"No LevelData for custom level {levelHash} running patch for spectator"); + var levelTask = mpBeatmapLevelProvider.GetBeatmap(levelHash); + __instance.countdownStarted = false; + __instance.StopListeningToGameStart(); // Ensure we stop listening for the start event while we run our start task + levelTask.ContinueWith(beatmapTask => + { + if (__instance.countdownStarted) return; // Another countdown has started, don't start the level + + BeatmapLevel? beatmapLevel = beatmapTask.Result?.MakeBeatmapLevel(gameplaySetupData.beatmapKey, + mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(levelHash)); + if (beatmapLevel == null) + beatmapLevel = new NoInfoBeatmapLevel(levelHash).MakeBeatmapLevel(gameplaySetupData.beatmapKey, + mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(levelHash)); + + __instance._menuTransitionsHelper.StartMultiplayerLevel("Multiplayer", gameplaySetupData.beatmapKey, beatmapLevel, beatmapLevelData, + __instance._playerDataModel.playerData.colorSchemesSettings.GetOverrideColorScheme(), gameplaySetupData.gameplayModifiers, + __instance._playerDataModel.playerData.playerSpecificSettings, null, Localization.Get("BUTTON_MENU"), false, + beforeSceneSwitchCallback, + __instance.HandleMultiplayerLevelDidFinish, + __instance.HandleMultiplayerLevelDidDisconnect + ); + }); + return false; + } + Plugin.Logger.Debug("LevelData present running orig"); + return true; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(LevelBar), nameof(LevelBar.Setup), new Type[] { typeof(BeatmapKey) })] + internal static bool LevelBar_Setup(LevelBar __instance, BeatmapKey beatmapKey) + { + var hash = Utilities.HashForLevelID(beatmapKey.levelId); + if (mpBeatmapLevelProvider != null && !string.IsNullOrWhiteSpace(hash)) + { + IPA.Utilities.Async.UnityMainThreadTaskScheduler.Factory.StartNew(async () => + { + BeatmapLevel? beatmapLevel = (await mpBeatmapLevelProvider.GetBeatmap(hash))?.MakeBeatmapLevel(beatmapKey, mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); + if (beatmapLevel == null) + beatmapLevel = new NoInfoBeatmapLevel(hash).MakeBeatmapLevel(beatmapKey, mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); + + __instance.SetupData(beatmapLevel, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic); + + }); + return false; + } + + return true; + } + + } +} From 0e25cd2a72442ec394d011370d609c142c2fbc41 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 17 Aug 2024 20:13:58 +0200 Subject: [PATCH 55/75] Update NodePoseSTM to use long and avoid unnecessary type casts --- .../NodePoseSyncState/MpNodePoseSyncStateManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStateManager.cs b/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStateManager.cs index c04dc9e..c71479c 100644 --- a/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStateManager.cs +++ b/MultiplayerCore/NodePoseSyncState/MpNodePoseSyncStateManager.cs @@ -8,8 +8,8 @@ namespace MultiplayerCore.NodePoseSyncState { internal class MpNodePoseSyncStateManager : IInitializable, IDisposable, IAffinity { - public float? DeltaUpdateFrequency { get; private set; } = null; - public float? FullStateUpdateFrequency { get; private set; } = null; + public long? DeltaUpdateFrequency { get; private set; } = null; + public long? FullStateUpdateFrequency { get; private set; } = null; private readonly MpPacketSerializer _packetSerializer; MpNodePoseSyncStateManager(MpPacketSerializer packetSerializer) => _packetSerializer = packetSerializer; @@ -33,7 +33,7 @@ private bool GetDeltaUpdateFrequencyMs(ref long __result) { if (DeltaUpdateFrequency.HasValue) { - __result = (long)(DeltaUpdateFrequency.Value); + __result = DeltaUpdateFrequency.Value; return false; } return true; @@ -45,7 +45,7 @@ private bool GetFullStateUpdateFrequencyMs(ref long __result) { if (FullStateUpdateFrequency.HasValue) { - __result = (long)(FullStateUpdateFrequency.Value); + __result = FullStateUpdateFrequency.Value; return false; } return true; From 5091e4849a667bec2975121162d3acdb63113d71 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 17 Aug 2024 20:14:28 +0200 Subject: [PATCH 56/75] Remove todo comment --- .../Patches/MultiplayerLevelFinishedControllerPatch.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs b/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs index b7ae202..7a49959 100644 --- a/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs +++ b/MultiplayerCore/Patches/MultiplayerLevelFinishedControllerPatch.cs @@ -16,7 +16,6 @@ static bool HandleRpcLevelFinished(MultiplayerLevelFinishedController __instance { // Possibly get notesCount from BeatSaver or by parsing the beatmapdata ourselves // Skip score validation if notesCount is 0, since custom songs always have notesCount 0 in BeatmapBasicData - // TODO: Change this to only be a single if (__instance._beatmapBasicData.notesCount <= 0 && results.hasAnyResults) if check if (__instance._beatmapBasicData.notesCount <= 0 && results.hasAnyResults) { Plugin.Logger.Info($"BeatmapData noteCount is 0, skipping validation"); From 368cdab73a06975706f1b66b845371c4d4beeadc Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 17 Aug 2024 20:15:03 +0200 Subject: [PATCH 57/75] Make entitlement checker public --- MultiplayerCore/Objects/MpEntitlementChecker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiplayerCore/Objects/MpEntitlementChecker.cs b/MultiplayerCore/Objects/MpEntitlementChecker.cs index a4953ef..3ab7996 100644 --- a/MultiplayerCore/Objects/MpEntitlementChecker.cs +++ b/MultiplayerCore/Objects/MpEntitlementChecker.cs @@ -11,7 +11,7 @@ namespace MultiplayerCore.Objects { - internal class MpEntitlementChecker : NetworkPlayerEntitlementChecker + public class MpEntitlementChecker : NetworkPlayerEntitlementChecker { public event Action? receivedEntitlementEvent; From cd13e5e2d8133ba25b9c72bf5b34d8953016d325 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 18 Aug 2024 17:38:17 +0200 Subject: [PATCH 58/75] Make LevelBar patch version dependand --- .../Patches/NoLevelSpectatorPatch.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs b/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs index b138f76..f439c36 100644 --- a/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs +++ b/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; using BGLib.Polyglot; using HarmonyLib; +using IPA.Utilities; using MultiplayerCore.Beatmaps; using MultiplayerCore.Beatmaps.Providers; using MultiplayerCore.Objects; @@ -16,7 +18,7 @@ namespace MultiplayerCore.Patches [HarmonyPatch] internal class NoLevelSpectatorPatch { - private static MpBeatmapLevelProvider? mpBeatmapLevelProvider; + internal static MpBeatmapLevelProvider? mpBeatmapLevelProvider; [HarmonyPrefix] [HarmonyPatch(typeof(LobbyGameStateController), nameof(LobbyGameStateController.StartMultiplayerLevel))] @@ -54,19 +56,28 @@ internal static bool LobbyGameStateController_StartMultiplayerLevel(LobbyGameSta Plugin.Logger.Debug("LevelData present running orig"); return true; } + } + + [HarmonyPatch] + internal class NoLevelSpectatorOptionalPatch + { + static bool Prepare() + { + return UnityGame.GameVersion >= new AlmostVersion("1.37.0"); + } [HarmonyPrefix] [HarmonyPatch(typeof(LevelBar), nameof(LevelBar.Setup), new Type[] { typeof(BeatmapKey) })] internal static bool LevelBar_Setup(LevelBar __instance, BeatmapKey beatmapKey) { var hash = Utilities.HashForLevelID(beatmapKey.levelId); - if (mpBeatmapLevelProvider != null && !string.IsNullOrWhiteSpace(hash)) + if (NoLevelSpectatorPatch.mpBeatmapLevelProvider != null && !string.IsNullOrWhiteSpace(hash) && SongCore.Loader.GetLevelByHash(hash) == null) { IPA.Utilities.Async.UnityMainThreadTaskScheduler.Factory.StartNew(async () => { - BeatmapLevel? beatmapLevel = (await mpBeatmapLevelProvider.GetBeatmap(hash))?.MakeBeatmapLevel(beatmapKey, mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); + BeatmapLevel? beatmapLevel = (await NoLevelSpectatorPatch.mpBeatmapLevelProvider.GetBeatmap(hash))?.MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch.mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); if (beatmapLevel == null) - beatmapLevel = new NoInfoBeatmapLevel(hash).MakeBeatmapLevel(beatmapKey, mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); + beatmapLevel = new NoInfoBeatmapLevel(hash).MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch.mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); __instance.SetupData(beatmapLevel, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic); @@ -76,6 +87,5 @@ internal static bool LevelBar_Setup(LevelBar __instance, BeatmapKey beatmapKey) return true; } - } } From e7efbe30b20be244ac58b3f6deb022cd0ac1f2ca Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Tue, 20 Aug 2024 22:39:10 +0200 Subject: [PATCH 59/75] Use Newtonsoft deserializer --- MultiplayerCore/Patches/MultiplayerStatusModelPatch.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Patches/MultiplayerStatusModelPatch.cs b/MultiplayerCore/Patches/MultiplayerStatusModelPatch.cs index d3ac0a4..906ea67 100644 --- a/MultiplayerCore/Patches/MultiplayerStatusModelPatch.cs +++ b/MultiplayerCore/Patches/MultiplayerStatusModelPatch.cs @@ -25,6 +25,6 @@ private static IEnumerable Transpiler(IEnumerable JsonUtility.FromJson(value); - } + => JsonConvert.DeserializeObject(value); + } } From 37df6c465400252aac36ee01b9bf5a0d7ffdf448 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Tue, 20 Aug 2024 22:40:20 +0200 Subject: [PATCH 60/75] Check status and override before enablind toggles --- MultiplayerCore/UI/MpPerPlayerUI.cs | 30 ++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs index 295cd3c..5f41abd 100644 --- a/MultiplayerCore/UI/MpPerPlayerUI.cs +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -11,10 +11,12 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using MultiplayerCore.Models; +using MultiplayerCore.Repositories; using UnityEngine; -using UnityEngine.EventSystems; using UnityEngine.UI; using Zenject; +using MultiplayerCore.Patchers; namespace MultiplayerCore.UI { @@ -29,9 +31,12 @@ internal class MpPerPlayerUI : IInitializable, IDisposable private readonly BeatmapLevelsModel _beatmapLevelsModel; private readonly MpPacketSerializer _packetSerializer; private readonly MpBeatmapLevelProvider _beatmapLevelProvider; + private readonly MpStatusRepository _statusRepository; + private readonly NetworkConfigPatcher _networkConfig; private BeatmapKey _currentBeatmapKey; private List? _allowedDiffs; private CanvasGroup? _difficultyCanvasGroup; + private MpStatusData? _currentStatusData; private readonly SiraLog _logger; @@ -41,6 +46,8 @@ public MpPerPlayerUI( IMultiplayerSessionManager sessionManager, MpBeatmapLevelProvider beatmapLevelProvider, MpPacketSerializer packetSerializer, + MpStatusRepository statusRepository, + NetworkConfigPatcher networkConfig, SiraLog logger) { _gameServerLobbyFlowCoordinator = gameServerLobbyFlowCoordinator; @@ -50,6 +57,8 @@ public MpPerPlayerUI( _multiplayerSessionManager = sessionManager; _beatmapLevelProvider = beatmapLevelProvider; _packetSerializer = packetSerializer; + _statusRepository = statusRepository; + _networkConfig = networkConfig; _logger = logger; } @@ -107,6 +116,8 @@ public void Initialize() _gameServerLobbyFlowCoordinator._serverPlayerListViewController.selectSuggestedBeatmapEvent += UpdateDifficultyListWithBeatmapKey; _lobbyViewController.clearSuggestedBeatmapEvent += ClearLocalSelectedBeatmap; _gameServerLobbyFlowCoordinator._lobbyPlayerPermissionsModel.permissionsChangedEvent += UpdateButtonsEnabled; + + _statusRepository.statusUpdatedForUrlEvent += HandleStatusUpdate; } public void Dispose() @@ -162,6 +173,8 @@ public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screen // Move toggles to correct position var locposition = _lobbyViewController._startGameReadyButton.gameObject.transform.localPosition; ppth!.gameObject.transform.localPosition = locposition; + + ppth.gameObject.SetActive(_networkConfig.IsOverridingApi && (_currentStatusData.supportsPPDifficulties || _currentStatusData.supportsPPModifiers)); } //public void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) @@ -172,6 +185,17 @@ public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screen // } //} + void HandleStatusUpdate(string statusUrl, MpStatusData statusData) + { + _logger.Info($"Got StatusData update for server {statusData.name} with values " + + $"supportsPPDifficulties={statusData.supportsPPDifficulties} and " + + $"supportsPPModifiers={statusData.supportsPPModifiers}"); + ppth?.gameObject.SetActive(statusData.supportsPPDifficulties || statusData.supportsPPModifiers); + ppdt?.gameObject.SetActive(statusData.supportsPPDifficulties); + ppmt?.gameObject.SetActive(statusData.supportsPPModifiers); + _currentStatusData = statusData; + } + #region DiffListUpdater // TODO: Possibly replace with BeatmapDifficultyMethods.Name ext method see BeatmapDifficultySegmentedControlController public string DiffToStr(BeatmapDifficulty difficulty) @@ -279,7 +303,7 @@ private void HandleGetMpPerPlayerPacket(GetMpPerPlayerPacket packet, IConnectedP var ppPacket = new MpPerPlayerPacket(); ppPacket.PPDEnabled = ppdt!.Value; ppPacket.PPMEnabled = ppmt!.Value; - _multiplayerSessionManager.Send(ppPacket); + _multiplayerSessionManager.SendToPlayer(ppPacket, _multiplayerSessionManager.connectionOwner); } private void UpdateButtonsEnabled() @@ -295,7 +319,7 @@ private void UpdateButtonsEnabled() ppth!.gameObject.transform.localPosition = locposition; // Request updated button states from server - _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); + _multiplayerSessionManager.SendToPlayer(new GetMpPerPlayerPacket(), _multiplayerSessionManager.connectionOwner); } private void SetLobbyState(MultiplayerLobbyState lobbyState) From 583a52a55447bc16a5c1d83a5fb7be6371f93638 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 24 Aug 2024 17:57:20 +0200 Subject: [PATCH 61/75] LevelDownloader install to custom folder when version 1.37.3+ --- MultiplayerCore/Objects/MpLevelDownloader.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MultiplayerCore/Objects/MpLevelDownloader.cs b/MultiplayerCore/Objects/MpLevelDownloader.cs index d01a476..df339bd 100644 --- a/MultiplayerCore/Objects/MpLevelDownloader.cs +++ b/MultiplayerCore/Objects/MpLevelDownloader.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using BeatSaverSharp; using BeatSaverSharp.Models; +using IPA.Utilities; using MultiplayerCore.Helpers; using SiraUtil.Logging; using SiraUtil.Zenject; @@ -15,7 +16,7 @@ namespace MultiplayerCore.Objects { internal class MpLevelDownloader { - public readonly string CustomLevelsFolder = Path.Combine(Application.dataPath, Plugin.CustomLevelsPath); + public readonly string CustomLevelsFolder = Path.Combine(Application.dataPath, UnityGame.GameVersion >= new AlmostVersion("1.37.3") ? Plugin.CustomLevelsPath : "CustomLevels"); private ConcurrentDictionary> _downloads = new(); private readonly ZipExtractor _zipExtractor = new(); From 5c7fc8dbbedb51f65bc394dba532820bf74cefdd Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 24 Aug 2024 17:58:16 +0200 Subject: [PATCH 62/75] Use Setup instead of SetupData --- MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs index 87d996c..ec6ec27 100644 --- a/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs +++ b/MultiplayerCore/Patchers/BeatmapSelectionViewPatcher.cs @@ -27,7 +27,7 @@ internal class BeatmapSelectionViewPatcher : IAffinity _mpBeatmapLevelProvider = mpBeatmapLevelProvider; _beatmapLevelsModel = beatmapLevelsModel; - _lbarInfo = AccessTools.Method(typeof(LevelBar), "SetupData", + _lbarInfo = AccessTools.Method(typeof(LevelBar), "Setup", new Type[] { typeof(BeatmapLevel), typeof(BeatmapDifficulty), typeof(BeatmapCharacteristicSO) }); if (_lbarInfo != null) _newlbarInfo = true; else _lbarInfo = AccessTools.Method(typeof(LevelBar), "Setup", new Type[] { typeof(BeatmapLevel), typeof(BeatmapCharacteristicSO), typeof(BeatmapDifficulty) }); @@ -95,7 +95,7 @@ IEnumerator SetBeatmapCoroutine(BeatmapSelectionView instance, BeatmapKey key, s instance._levelBar.hide = false; - Plugin.Logger.Debug($"Calling Setup/SetupData with level type: {level.GetType().Name}, beatmapCharacteristic type: {key.beatmapCharacteristic.GetType().Name}, difficulty type: {key.difficulty.GetType().Name} "); + Plugin.Logger.Debug($"Calling Setup with level type: {level.GetType().Name}, beatmapCharacteristic type: {key.beatmapCharacteristic.GetType().Name}, difficulty type: {key.difficulty.GetType().Name} "); if (_newlbarInfo) { _lbarInfo.Invoke(instance._levelBar, new object[] { level, key.difficulty, key.beatmapCharacteristic }); From a92270e88428d623e6c6446ecbc37896f64669ee Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 24 Aug 2024 17:58:48 +0200 Subject: [PATCH 63/75] Bump up game version --- MultiplayerCore/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiplayerCore/manifest.json b/MultiplayerCore/manifest.json index 8081eee..efb722c 100644 --- a/MultiplayerCore/manifest.json +++ b/MultiplayerCore/manifest.json @@ -5,7 +5,7 @@ "author": "Goobwabber", "version": "1.5.0", "description": "Adds custom songs to Beat Saber Multiplayer.", - "gameVersion": "1.37.2", + "gameVersion": "1.37.3", "dependsOn": { "BSIPA": "^4.3.3", "SongCore": "^3.13.0", From 264f6d67ea962d4edf0d55708e94f63d956e77fb Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 24 Aug 2024 18:00:23 +0200 Subject: [PATCH 64/75] Per player modifiers, disable song speed modifiers --- MultiplayerCore/UI/MpPerPlayerUI.cs | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs index 5f41abd..019c7f3 100644 --- a/MultiplayerCore/UI/MpPerPlayerUI.cs +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -105,6 +105,8 @@ public void Initialize() } _lobbyViewController.didActivateEvent += DidActivate; + _gameServerLobbyFlowCoordinator._selectModifiersViewController.didActivateEvent += + ModifierSelectionDidActivate; //_lobbyViewController.didDeactivateEvent += DidDeactivate; _packetSerializer.RegisterCallback(HandleMpPerPlayerPacket); @@ -123,6 +125,8 @@ public void Initialize() public void Dispose() { _lobbyViewController.didActivateEvent -= DidActivate; + _gameServerLobbyFlowCoordinator._selectModifiersViewController.didActivateEvent -= + ModifierSelectionDidActivate; //_lobbyViewController.didDeactivateEvent -= DidDeactivate; _packetSerializer.UnregisterCallback(); @@ -177,6 +181,28 @@ public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screen ppth.gameObject.SetActive(_networkConfig.IsOverridingApi && (_currentStatusData.supportsPPDifficulties || _currentStatusData.supportsPPModifiers)); } + void ModifierSelectionDidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) + { + _logger.Trace("ModifierSelectionDidActivate"); + var modifierController = _gameServerLobbyFlowCoordinator._selectModifiersViewController + ._gameplayModifiersPanelController; + //modifierController._gameplayModifiers = + // modifierController.gameplayModifiers.CopyWith(songSpeed: GameplayModifiers.SongSpeed.Normal); + var toggles = modifierController._gameplayModifierToggles; + foreach (var toggle in toggles) + { + _logger.Trace("Toggle: " + toggle.gameObject.name); + if (toggle.gameObject.name == "FasterSong" || toggle.gameObject.name == "SuperFastSong" || + toggle.gameObject.name == "SlowerSong") + { + toggle.toggle.interactable = !ppmt.Value; + var canvas = toggle.gameObject.GetComponent(); + if (canvas == null) canvas = toggle.gameObject.AddComponent(); + canvas.alpha = ppmt.Value ? 0.25f : 1f; + } + } + } + //public void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) //{ // if (removedFromHierarchy) @@ -294,6 +320,21 @@ private void HandleMpPerPlayerPacket(MpPerPlayerPacket packet, IConnectedPlayer { _logger.Warn("Player is not Connection Owner, ignoring packet"); } + + if (player.isConnectionOwner) + { + // If a SongSpeed modifier was already set, remove it and re-announce our modifiers + var modifierController = _gameServerLobbyFlowCoordinator._selectModifiersViewController + ._gameplayModifiersPanelController; + if (modifierController != null && modifierController.gameplayModifiers != null && + modifierController.gameplayModifiers.songSpeed != GameplayModifiers.SongSpeed.Normal) + { + modifierController._gameplayModifiers = + modifierController.gameplayModifiers.CopyWith(songSpeed: GameplayModifiers.SongSpeed.Normal); + _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel.SetLocalPlayerGameplayModifiers( + modifierController.gameplayModifiers); + } + } } private void HandleGetMpPerPlayerPacket(GetMpPerPlayerPacket packet, IConnectedPlayer player) From ef81ba5e657626b593b00785c6a3988e8dfdcf0e Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sat, 24 Aug 2024 18:02:36 +0200 Subject: [PATCH 65/75] NoLevelSpectator, patch ResultView instead of LevelBar --- .../Patches/NoLevelSpectatorPatch.cs | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs b/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs index f439c36..df5aa9b 100644 --- a/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs +++ b/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs @@ -11,7 +11,6 @@ using MultiplayerCore.Beatmaps.Providers; using MultiplayerCore.Objects; using MultiplayerCore.Patchers; -using Zenject; namespace MultiplayerCore.Patches { @@ -61,31 +60,47 @@ internal static bool LobbyGameStateController_StartMultiplayerLevel(LobbyGameSta [HarmonyPatch] internal class NoLevelSpectatorOptionalPatch { + private static MethodInfo _lbarInfo; + private static bool _newlbarInfo; static bool Prepare() { - return UnityGame.GameVersion >= new AlmostVersion("1.37.0"); + _lbarInfo = AccessTools.Method(typeof(LevelBar), "Setup", + new Type[] { typeof(BeatmapLevel), typeof(BeatmapDifficulty), typeof(BeatmapCharacteristicSO) }); + if (_lbarInfo != null) _newlbarInfo = true; + else _lbarInfo = AccessTools.Method(typeof(LevelBar), "Setup", new Type[] { typeof(BeatmapLevel), typeof(BeatmapCharacteristicSO), typeof(BeatmapDifficulty) }); + if (_lbarInfo == null) + { + Plugin.Logger.Critical("Can't find a fitting LevelBar Method, is your game version supported?"); + return false; + } + + return true; } - [HarmonyPrefix] - [HarmonyPatch(typeof(LevelBar), nameof(LevelBar.Setup), new Type[] { typeof(BeatmapKey) })] - internal static bool LevelBar_Setup(LevelBar __instance, BeatmapKey beatmapKey) + [HarmonyPostfix] + [HarmonyPatch(typeof(MultiplayerResultsViewController), nameof(MultiplayerResultsViewController.Init))] + internal static void MultiplayerResultsViewController_Init(MultiplayerResultsViewController __instance, BeatmapKey beatmapKey) { var hash = Utilities.HashForLevelID(beatmapKey.levelId); - if (NoLevelSpectatorPatch.mpBeatmapLevelProvider != null && !string.IsNullOrWhiteSpace(hash) && SongCore.Loader.GetLevelByHash(hash) == null) + if (NoLevelSpectatorPatch.mpBeatmapLevelProvider != null && !string.IsNullOrWhiteSpace(hash) && + SongCore.Loader.GetLevelByHash(hash) == null) { IPA.Utilities.Async.UnityMainThreadTaskScheduler.Factory.StartNew(async () => { BeatmapLevel? beatmapLevel = (await NoLevelSpectatorPatch.mpBeatmapLevelProvider.GetBeatmap(hash))?.MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch.mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); if (beatmapLevel == null) beatmapLevel = new NoInfoBeatmapLevel(hash).MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch.mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); - - __instance.SetupData(beatmapLevel, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic); - + Plugin.Logger.Trace($"Calling Setup with level type: {beatmapLevel.GetType().Name}, beatmapCharacteristic type: {beatmapKey.beatmapCharacteristic.GetType().Name}, difficulty type: {beatmapKey.difficulty.GetType().Name} "); + if (_newlbarInfo) + { + _lbarInfo.Invoke(__instance._levelBar, new object[] { beatmapLevel, beatmapKey.difficulty, beatmapKey.beatmapCharacteristic }); + } + else + { + _lbarInfo.Invoke(__instance._levelBar, new object[] { beatmapLevel, beatmapKey.beatmapCharacteristic, beatmapKey.difficulty }); + } }); - return false; } - - return true; } } } From 6a908ec7de407af93b2e93f6a875cf2c3881cb5c Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 25 Aug 2024 19:09:15 +0200 Subject: [PATCH 66/75] BeatSaver bm ignore Chroma, MpPerPlayerUI unregister status callback on dispose --- MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs | 4 ++-- MultiplayerCore/UI/MpPerPlayerUI.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs b/MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs index f4d9748..cd6df91 100644 --- a/MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs +++ b/MultiplayerCore/Beatmaps/BeatSaverBeatmapLevel.cs @@ -46,8 +46,8 @@ public override Dictionary> Requ if (!reqs.ContainsKey(characteristic)) reqs.Add(characteristic, new()); string[] diffReqs = new string[0]; - if (difficulty.Chroma) - diffReqs.Append("Chroma"); + //if (difficulty.Chroma) + // diffReqs.Append("Chroma"); if (difficulty.NoodleExtensions) diffReqs.Append("Noodle Extensions"); if (difficulty.MappingExtensions) diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs index 019c7f3..afc9df3 100644 --- a/MultiplayerCore/UI/MpPerPlayerUI.cs +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -131,6 +131,8 @@ public void Dispose() _packetSerializer.UnregisterCallback(); _packetSerializer.UnregisterCallback(); + + _statusRepository.statusUpdatedForUrlEvent -= HandleStatusUpdate; } public void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) From a11f840b94d2e23dabcbc2ebfa38d3928df4e097 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 25 Aug 2024 19:11:15 +0200 Subject: [PATCH 67/75] MpPlayerData default initialize GameVersion with current gameversion, MpPlayerManager fixed not sending local MpPlayerData --- MultiplayerCore/Players/MpPlayerData.cs | 8 ++++--- MultiplayerCore/Players/MpPlayerManager.cs | 26 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/MultiplayerCore/Players/MpPlayerData.cs b/MultiplayerCore/Players/MpPlayerData.cs index a898d7b..62dc47f 100644 --- a/MultiplayerCore/Players/MpPlayerData.cs +++ b/MultiplayerCore/Players/MpPlayerData.cs @@ -1,7 +1,7 @@ using Hive.Versioning; using LiteNetLib.Utils; using MultiplayerCore.Networking.Abstractions; -using System.Collections.Generic; +using IPA.Utilities; namespace MultiplayerCore.Players { @@ -20,7 +20,7 @@ public class MpPlayerData : MpPacket /// /// Version /// - public Version GameVersion { get; set; } + public Version GameVersion { get; set; } = Version.Parse(UnityGame.GameVersion.ToString().Split('_')[0]); public override void Serialize(NetDataWriter writer) { @@ -43,6 +43,8 @@ public enum Platform Steam = 1, OculusPC = 2, OculusQuest = 3, - PS4 = 4 + PS4 = 4, + PS5 = 5, + Pico = 6 } } diff --git a/MultiplayerCore/Players/MpPlayerManager.cs b/MultiplayerCore/Players/MpPlayerManager.cs index ce9f98a..473ec15 100644 --- a/MultiplayerCore/Players/MpPlayerManager.cs +++ b/MultiplayerCore/Players/MpPlayerManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Threading; using Zenject; namespace MultiplayerCore.Players @@ -17,19 +18,25 @@ public class MpPlayerManager : IInitializable, IDisposable private readonly MpPacketSerializer _packetSerializer; private readonly IMultiplayerSessionManager _sessionManager; + private readonly IPlatformUserModel _platformUserModel; internal MpPlayerManager( MpPacketSerializer packetSerializer, - IMultiplayerSessionManager sessionManager) + IMultiplayerSessionManager sessionManager, + IPlatformUserModel platformUserModel) { _packetSerializer = packetSerializer; _sessionManager = sessionManager; + _platformUserModel = platformUserModel; } public async void Initialize() { _sessionManager.SetLocalPlayerState("modded", true); _packetSerializer.RegisterCallback(HandlePlayerData); + _sessionManager.playerConnectedEvent += HandlePlayerConnected; + + _localPlayerInfo = await _platformUserModel.GetUserInfo(CancellationToken.None); } public void Dispose() @@ -37,6 +44,23 @@ public void Dispose() _packetSerializer.UnregisterCallback(); } + private void HandlePlayerConnected(IConnectedPlayer player) + { + if (_localPlayerInfo == null) + throw new NullReferenceException("local player info was not yet set! make sure it is set before anything else happens!"); + + _sessionManager.Send(new MpPlayerData + { + Platform = _localPlayerInfo.platform switch + { + UserInfo.Platform.Oculus => Platform.OculusPC, + UserInfo.Platform.Steam => Platform.Steam, + _ => Platform.Unknown + }, + PlatformId = _localPlayerInfo.platformUserId + }); + } + private void HandlePlayerData(MpPlayerData packet, IConnectedPlayer player) { _playerData[player.userId] = packet; From 8739fec5e7012aa845c2c89f92f6570f71cdeea4 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 25 Aug 2024 19:13:01 +0200 Subject: [PATCH 68/75] Removed unused UI classes --- .../UI/LobbySettingsViewController.bsml | 6 - .../UI/LobbySettingsViewController.cs | 64 --- MultiplayerCore/UI/LobbySetupPanel.bsml | 18 - MultiplayerCore/UI/LobbySetupPanel.cs | 426 ------------------ MultiplayerCore/UI/MpCoreGameplaySetup.bsml | 6 - MultiplayerCore/UI/MpCoreGameplaySetup.cs | 104 ----- .../UI/MpCoreSetupFlowCoordinator.cs | 62 --- 7 files changed, 686 deletions(-) delete mode 100644 MultiplayerCore/UI/LobbySettingsViewController.bsml delete mode 100644 MultiplayerCore/UI/LobbySettingsViewController.cs delete mode 100644 MultiplayerCore/UI/LobbySetupPanel.bsml delete mode 100644 MultiplayerCore/UI/LobbySetupPanel.cs delete mode 100644 MultiplayerCore/UI/MpCoreGameplaySetup.bsml delete mode 100644 MultiplayerCore/UI/MpCoreGameplaySetup.cs delete mode 100644 MultiplayerCore/UI/MpCoreSetupFlowCoordinator.cs diff --git a/MultiplayerCore/UI/LobbySettingsViewController.bsml b/MultiplayerCore/UI/LobbySettingsViewController.bsml deleted file mode 100644 index d607eb5..0000000 --- a/MultiplayerCore/UI/LobbySettingsViewController.bsml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/MultiplayerCore/UI/LobbySettingsViewController.cs b/MultiplayerCore/UI/LobbySettingsViewController.cs deleted file mode 100644 index ad2da57..0000000 --- a/MultiplayerCore/UI/LobbySettingsViewController.cs +++ /dev/null @@ -1,64 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.ViewControllers; -using IPA.Utilities; -using Zenject; - -namespace MultiplayerCore.UI -{ - [ViewDefinition("MultiplayerCore.UI.LobbySettingsViewController.bsml")] - public class LobbySettingsViewController : BSMLAutomaticViewController - { - private FieldAccessor.Accessor _showModifiers - = FieldAccessor.GetAccessor(nameof(_showModifiers)); - private FieldAccessor.Accessor _showEnvironmentOverrideSettings - = FieldAccessor.GetAccessor(nameof(_showEnvironmentOverrideSettings)); - private FieldAccessor.Accessor _showColorSchemesSettings - = FieldAccessor.GetAccessor(nameof(_showColorSchemesSettings)); - private FieldAccessor.Accessor _showMultiplayer - = FieldAccessor.GetAccessor(nameof(_showMultiplayer)); - - private GameplaySetupViewController _gameplaySetup = null!; - private bool _perPlayerDiffs = false; - private bool _perPlayerModifiers = false; - //private Config _config = null!; - - [Inject] - private void Construct( - GameplaySetupViewController gameplaySetup/*, - Config config*/) - { - _gameplaySetup = gameplaySetup; - //_config = config; - } - - [UIAction("#post-parse")] - private void PostParse() - { - //_sideBySideDistanceIncrement.interactable = _sideBySide; - } - - [UIValue("per-player-diffs")] - public bool PerPlayerDifficulty - { - //get => _config.SoloEnvironment; - get => _perPlayerDiffs; - set - { - _perPlayerDiffs = value; - NotifyPropertyChanged(); - } - } - - [UIValue("per-player-modifiers")] - public bool PerPlayerModifiers - { - get => _perPlayerModifiers; - set - { - _perPlayerModifiers = value; - NotifyPropertyChanged(); - } - } - } -} diff --git a/MultiplayerCore/UI/LobbySetupPanel.bsml b/MultiplayerCore/UI/LobbySetupPanel.bsml deleted file mode 100644 index a6df7b7..0000000 --- a/MultiplayerCore/UI/LobbySetupPanel.bsml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/MultiplayerCore/UI/LobbySetupPanel.cs b/MultiplayerCore/UI/LobbySetupPanel.cs deleted file mode 100644 index 3188fd9..0000000 --- a/MultiplayerCore/UI/LobbySetupPanel.cs +++ /dev/null @@ -1,426 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.ViewControllers; -using HMUI; -using IPA.Utilities.Async; -using MultiplayerCore.Beatmaps.Abstractions; -using MultiplayerCore.Beatmaps.Providers; -using MultiplayerCore.Networking; -using MultiplayerCore.Players.Packets; -using UnityEngine; -using UnityEngine.PlayerLoop; -using UnityEngine.UI; -using Zenject; -using static IPA.Logging.Logger; - -namespace MultiplayerCore.UI -{ - public class LobbySetupPanel : BSMLResourceViewController - { - public override string ResourceName => "MultiplayerCore.UI.LobbySetupPanel.bsml"; - private GameServerLobbyFlowCoordinator _gameServerLobbyFlowCoordinator; - private LobbySetupViewController _lobbyViewController; - private BeatmapLevelsModel _beatmapLevelsModel; - private IMultiplayerSessionManager _multiplayerSessionManager; - private ILobbyGameStateController _gameStateController; - private MpPacketSerializer _packetSerializer; - private MpBeatmapLevelProvider _beatmapLevelProvider; - private BeatmapKey _currentBeatmapKey; - private bool _perPlayerDiffs = false; - private bool _perPlayerModifiers = false; - private List _allowedDiffs; - private CanvasGroup? _difficultyCanvasGroup; - - //BeatSaberMarkupLanguage.Tags.TextSegmentedControlTag - - [Inject] - internal void Inject(GameServerLobbyFlowCoordinator gameServerLobbyFlowCoordinator, BeatmapLevelsModel beatmapLevelsModel, IMultiplayerSessionManager sessionManager, MpBeatmapLevelProvider beatmapLevelProvider, MpPacketSerializer packetSerializer) - { - DidActivate(true, false, false); - _gameServerLobbyFlowCoordinator = gameServerLobbyFlowCoordinator; - _lobbyViewController = _gameServerLobbyFlowCoordinator._lobbySetupViewController; - _gameStateController = _gameServerLobbyFlowCoordinator._lobbyGameStateController; - _beatmapLevelsModel = beatmapLevelsModel; - _multiplayerSessionManager = sessionManager; - _beatmapLevelProvider = beatmapLevelProvider; - _packetSerializer = packetSerializer; - - _lobbyViewController.didActivateEvent += DidActivate; - _lobbyViewController.didDeactivateEvent += DidDeactivate; - - - // TODO: Possibly adjust position based on enabled UI elements - var cgubPos = _lobbyViewController._cancelGameUnreadyButton.transform.position; - cgubPos.y -= 0.4f; - _lobbyViewController._cancelGameUnreadyButton.transform.position = cgubPos; - - var sgrbPos = _lobbyViewController._startGameReadyButton.transform.position; - sgrbPos.y -= 0.4f; - _lobbyViewController._startGameReadyButton.transform.position = sgrbPos; - - var csgHHPos = _lobbyViewController._cantStartGameHoverHint.transform.position; - csgHHPos.y -= 0.4f; - _lobbyViewController._cantStartGameHoverHint.transform.position = csgHHPos; - - //if (!_lobbyViewController._isPartyOwner) - //{ - // var diffPos = difficulty.transform.position; - // diffPos.y -= -0.2f; - - //} - - // TODO: Proper registration - _packetSerializer.RegisterCallback(HandleMpPerPlayerPacket); - _packetSerializer.RegisterType(); - - //var stwParentPos = _lobbyViewController._spectatorWarningTextWrapper.transform.position; - //stwParentPos.y -= 0.5f; - //_lobbyViewController._spectatorWarningTextWrapper.transform.position = stwParentPos; - } - - protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) - { - Plugin.Logger.Debug($"LobbySetup DidActivate called! {firstActivation}, {addedToHierarchy}, {screenSystemEnabling}"); - base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); - - if (_gameStateController == null || _lobbyViewController == null || perPlayerDiffsToggle == null || - perPlayerModifiersToggle == null) - { - Plugin.Logger.Debug($"One object was null {_gameStateController}, {_lobbyViewController}, {perPlayerDiffsToggle}, {perPlayerModifiersToggle}"); - return; - } - // Make sure to only allow selecting difficulties that are enabled for the lobby - if (!firstActivation) - { - var lobbyPlayersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as LobbyPlayersDataModel; - LobbyPlayerData? playerData = - lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, - out _); - if (playerData != null) - UpdateDifficultyList(playerData.beatmapKey); - } - - if (!firstActivation && addedToHierarchy) - { - // Reset our buttons - _perPlayerDiffs = false; - _perPlayerModifiers = false; - UpdateButtonValues(); - // Request Updated state - _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); - } - //else if (_gameStateController.selectedLevelGameplaySetupData.beatmapKey.IsValid()) - // _currentBeatmapKey = _gameStateController.selectedLevelGameplaySetupData.beatmapKey; - //if (_currentBeatmapKey.IsValid()) UpdateDifficultyList(_currentBeatmapKey); - //else segmentVert.gameObject.SetActive(false); - - // We register the callbacks - _gameStateController.lobbyStateChangedEvent += SetLobbyState; - //_gameStateController.selectedLevelGameplaySetupDataChangedEvent += HostSelectedBeatmap; - _gameServerLobbyFlowCoordinator._multiplayerLevelSelectionFlowCoordinator.didSelectLevelEvent += LocalSelectedBeatmap; - _gameServerLobbyFlowCoordinator._serverPlayerListViewController.selectSuggestedBeatmapEvent += UpdateDifficultyList; - _lobbyViewController.clearSuggestedBeatmapEvent += ClearLocalSelectedBeatmap; - _gameServerLobbyFlowCoordinator._lobbyPlayerPermissionsModel.permissionsChangedEvent += - UpdateButtonsEnabled; - - //else UpdateDifficultyList(Enum.GetValues(typeof(BeatmapDifficulty)).Cast().ToList()); - - //List difficultyList = - // Enum.GetValues(typeof(BeatmapDifficulty)).Cast().ToList(); - //var levelId = _gameStateController.selectedLevelGameplaySetupData.beatmapKey.levelId; - //var characteristic = _gameStateController.selectedLevelGameplaySetupData.beatmapKey.beatmapCharacteristic; - //_allowedDiffs = (from diff in difficultyList - // where _gameServerLobbyFlowCoordinator._unifiedNetworkPlayerModel.selectionMask.difficulties - // .Contains(diff) - // select diff.ToString().Replace("ExpertPlus", "Expert+")).ToList(); - //foreach (var difficulty in _allowedDiffs) - // Plugin.Logger.Debug($"Allowed difficulty='{difficulty}'"); - - if (_lobbyViewController._isPartyOwner) - { - perPlayerDiffsToggle.gameObject.SetActive(true); - perPlayerModifiersToggle.gameObject.SetActive(true); - perPlayerDiffsToggle.interactable = true; - perPlayerModifiersToggle.interactable = true; - } - else - { - perPlayerDiffsToggle.gameObject.SetActive(false); - perPlayerModifiersToggle.gameObject.SetActive(false); - } - } - - protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) - { - base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); - Plugin.Logger.Debug($"LobbySetup DidDeactivate called! {removedFromHierarchy}, {screenSystemDisabling}"); - if (removedFromHierarchy) - { - _gameStateController.lobbyStateChangedEvent -= SetLobbyState; - //_gameStateController.selectedLevelGameplaySetupDataChangedEvent -= HostSelectedBeatmap; - _gameServerLobbyFlowCoordinator._multiplayerLevelSelectionFlowCoordinator.didSelectLevelEvent -= LocalSelectedBeatmap; - _gameServerLobbyFlowCoordinator._serverPlayerListViewController.selectSuggestedBeatmapEvent -= UpdateDifficultyList; - _lobbyViewController.clearSuggestedBeatmapEvent -= ClearLocalSelectedBeatmap; - _gameServerLobbyFlowCoordinator._lobbyPlayerPermissionsModel.permissionsChangedEvent -= - UpdateButtonsEnabled; - } - } - - private void HandleMpPerPlayerPacket(MpPerPlayerPacket packet, IConnectedPlayer player) - { - Plugin.Logger.Debug($"Got MpPerPlayerPacket from {player.userName}|{player.userId} with values PPDEnabled={packet.PPDEnabled}, PPMEnabled={packet.PPMEnabled}"); - if (packet.PPDEnabled != PerPlayerDifficulty || packet.PPMEnabled != _perPlayerModifiers) - { - _perPlayerDiffs = packet.PPDEnabled; - _perPlayerModifiers = packet.PPMEnabled; - //perPlayerDiffsToggle.Value = _perPlayerDiffs; - //perPlayerModifiersToggle.Value = _perPlayerModifiers; - UpdateButtonValues(); - } - } - - public string DiffToStr(BeatmapDifficulty difficulty) - { - return difficulty == BeatmapDifficulty.ExpertPlus ? "Expert+" : difficulty.ToString(); - } - - //public List DiffsToStrs(BeatmapDifficulty[] difficulties) => - // difficulties.Select(diff => DiffToStr(diff)).ToList(); - - private void UpdateDifficultyList(BeatmapKey beatmapKey) - { - _currentBeatmapKey = beatmapKey; - if (!_currentBeatmapKey.IsValid()) - { - segmentVert.gameObject.SetActive(false); - return; - } - var levelHash = Utilities.HashForLevelID(beatmapKey.levelId); - if (levelHash != null) - { - Plugin.Logger.Debug($"Level is custom, trying to get beatmap for hash {levelHash}"); - _beatmapLevelProvider.GetBeatmap(levelHash).ContinueWith(levelTask => - { - if (levelTask.IsCompleted && !levelTask.IsFaulted && levelTask.Result != null) - { - var level = levelTask.Result; - Plugin.Logger.Debug($"Got level {level.LevelHash}, {level.Requirements}, {level.Requirements[beatmapKey.beatmapCharacteristic.serializedName]}"); - // Hacky we use requirements to get the available difficulties - UpdateDifficultyList(level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Keys - .ToList()); - } - }); - } - else - { - Plugin.Logger.Debug($"LevelId not custom: {beatmapKey.levelId}, getting difficulties from basegame"); - var diffList = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId) - ?.GetDifficulties(beatmapKey.beatmapCharacteristic).ToList(); - if (diffList != null) UpdateDifficultyList(diffList); - } - } - - private void UpdateDifficultyList(IReadOnlyList difficulties) - { - // Ensure that we run on the UnityMainThread here - UnityMainThreadTaskScheduler.Factory.StartNew(() => - { - _allowedDiffs = (from diff in difficulties - where _gameServerLobbyFlowCoordinator._unifiedNetworkPlayerModel.selectionMask.difficulties - .Contains(diff) - select DiffToStr(diff) - ).ToList(); - foreach (var difficultyStr in _allowedDiffs) - Plugin.Logger.Debug($"Allowed difficulty='{difficultyStr}'"); - - if (_allowedDiffs.Count > 1) - { - segmentVert.gameObject.SetActive(true); - difficulty.SetTexts(_allowedDiffs); - int index = _allowedDiffs.IndexOf(DiffToStr(_currentBeatmapKey.difficulty)); - if (index > 0) - difficulty.SelectCellWithNumber(index); - } - else segmentVert.gameObject.SetActive(false); - } - ); - } - - private void SetLobbyState(MultiplayerLobbyState lobbyState) - { - Plugin.Logger.Debug($"Current Lobby State {lobbyState}"); - enableUserInteractions = lobbyState == MultiplayerLobbyState.LobbySetup || - lobbyState == MultiplayerLobbyState.LobbyCountdown; - - if (_difficultyCanvasGroup == null) - _difficultyCanvasGroup = difficulty?.gameObject.AddComponent(); - if (_difficultyCanvasGroup != null) - _difficultyCanvasGroup.alpha = (lobbyState == MultiplayerLobbyState.LobbySetup || - lobbyState == MultiplayerLobbyState.LobbyCountdown) ? 1f : 0.25f; - //var canvasGroup = segmentVert.GetComponentInParent(); - //var ourCanvasGroup = GetComponent(); - //if (ourCanvasGroup != null) - // Plugin.Logger.Debug($"CanvasGroup found in parent"); - //else Plugin.Logger.Error($"CanvasGroup was null!"); - //if (ourCanvasGroup != null) - // //UnityMainThreadTaskScheduler.Factory.StartNew(async () => - // //{ - // //await Task.Delay(2000); - // ourCanvasGroup.alpha = lobbyState == MultiplayerLobbyState.LobbySetup ? 1f : 0.25f; - // //}); - //segmentVert. = (lobbyState == MultiplayerLobbyState.LobbySetup); - if (_lobbyViewController == null) - return; - - if (_lobbyViewController._isPartyOwner) - { - perPlayerDiffsToggle.interactable = lobbyState == MultiplayerLobbyState.LobbySetup; - //perPlayerDiffsToggle.gameObject.SetActive(lobbyState == MultiplayerLobbyState.LobbySetup); - - perPlayerModifiersToggle.interactable = lobbyState == MultiplayerLobbyState.LobbySetup; - //perPlayerModifiersToggle.gameObject.SetActive(lobbyState == MultiplayerLobbyState.LobbySetup); - - } - } - - private void LocalSelectedBeatmap(LevelSelectionFlowCoordinator.State state) - { - _currentBeatmapKey = state.beatmapKey; - UpdateDifficultyList(state.beatmapLevel.GetDifficulties(state.beatmapKey.beatmapCharacteristic).ToList()); - //var lobbyPlayersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as LobbyPlayersDataModel; - //LobbyPlayerData? playerData = - // lobbyPlayersDataModel?.GetOrCreateLobbyPlayerDataModel(lobbyPlayersDataModel.localUserId, - // out _); - //if (playerData != null && playerData.beatmapKey.IsValid()) - // UpdateDifficultyList(playerData.beatmapKey); - } - - //private void HostSelectedBeatmap(ILevelGameplaySetupData gameplaySetupData) - //{ - // UpdateDifficultyList(gameplaySetupData.beatmapKey); - //} - - private void ClearLocalSelectedBeatmap() - { - segmentVert.gameObject.SetActive(false); - _currentBeatmapKey = new BeatmapKey(); - } - - private void UpdateButtonsEnabled() - { - if (_lobbyViewController._isPartyOwner) - { - perPlayerDiffsToggle.gameObject.SetActive(true); - perPlayerModifiersToggle.gameObject.SetActive(true); - // Request updated button states from server - _multiplayerSessionManager.Send(new GetMpPerPlayerPacket()); - } - else - { - perPlayerDiffsToggle.gameObject.SetActive(false); - perPlayerModifiersToggle.gameObject.SetActive(false); - } - } - - private void UpdateButtonValues() - { - perPlayerDiffsToggle.Value = _perPlayerDiffs; - perPlayerModifiersToggle.Value = _perPlayerModifiers; - } - - - #region UIComponents - - [UIComponent("ppdt")] - public ToggleSetting perPlayerDiffsToggle; - - [UIComponent("ppmt")] - public ToggleSetting perPlayerModifiersToggle; - - [UIComponent("difficulty-control")] - public TextSegmentedControl difficulty; - - [UIComponent("segment-vert")] - public VerticalLayoutGroup segmentVert; - - #endregion - - #region UIValues - - [UIValue("per-player-diffs")] - public bool PerPlayerDifficulty - { - get => _perPlayerDiffs; - set - { - _perPlayerDiffs = value; - _multiplayerSessionManager.Send(new MpPerPlayerPacket - { - PPDEnabled = _perPlayerDiffs, - PPMEnabled = _perPlayerModifiers - }); - NotifyPropertyChanged(); - } - } - - [UIValue("per-player-modifiers")] - public bool PerPlayerModifiers - { - get => _perPlayerModifiers; - set - { - _perPlayerModifiers = value; - _multiplayerSessionManager.Send(new MpPerPlayerPacket - { - PPDEnabled = _perPlayerDiffs, - PPMEnabled = _perPlayerModifiers - }); - NotifyPropertyChanged(); - } - } - - //[UIAction("per-player-modifiers-changed")] - //public void OnPerPlayerModifiersChanged(bool value) - //{ - // _perPlayerModifiers = value; - // _multiplayerSessionManager.Send(new MpPerPlayerPacket - // { - // PPDEnabled = _perPlayerDiffs, - // PPMEnabled = _perPlayerModifiers - // }); - // Plugin.Logger.Debug($"Sending MpPerPlayerPacket Packet with values: PPDEnabled='{_perPlayerDiffs}', PPMEnabled='{_perPlayerModifiers}'"); - //} - - //[UIAction("per-player-diffs-changed")] - //public void OnPerPlayerDifficultyChanged(bool value) - //{ - // _perPlayerDiffs = value; - // _multiplayerSessionManager.Send(new MpPerPlayerPacket - // { - // PPDEnabled = _perPlayerDiffs, - // PPMEnabled = _perPlayerModifiers - // }); - // Plugin.Logger.Debug($"Sending MpPerPlayerPacket Packet with values: PPDEnabled='{_perPlayerDiffs}', PPMEnabled='{_perPlayerModifiers}'"); - //} - - - [UIAction("difficulty-selected")] - public void SetSelectedDifficulty(TextSegmentedControl _, int index) - { - var diff = _allowedDiffs[index]; - Plugin.Logger.Debug($"Selected difficulty at {index} - {diff}"); - if (Enum.TryParse(diff.Replace("Expert+", "ExpertPlus"), out BeatmapDifficulty difficulty)) - { - _currentBeatmapKey = new BeatmapKey(_currentBeatmapKey.levelId, - _currentBeatmapKey.beatmapCharacteristic, difficulty); - _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel.SetLocalPlayerBeatmapLevel(_currentBeatmapKey); - } - } - #endregion - } -} diff --git a/MultiplayerCore/UI/MpCoreGameplaySetup.bsml b/MultiplayerCore/UI/MpCoreGameplaySetup.bsml deleted file mode 100644 index 6c4ed65..0000000 --- a/MultiplayerCore/UI/MpCoreGameplaySetup.bsml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/MultiplayerCore/UI/MpCoreGameplaySetup.cs b/MultiplayerCore/UI/MpCoreGameplaySetup.cs deleted file mode 100644 index 44dbd41..0000000 --- a/MultiplayerCore/UI/MpCoreGameplaySetup.cs +++ /dev/null @@ -1,104 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using HMUI; -using SiraUtil.Logging; -using System; -using System.Reflection; -using UnityEngine; -using Zenject; - -namespace MultiplayerCore.UI -{ - public class MpCoreGameplaySetup : NotifiableBase, IInitializable, IDisposable - { - public const string ResourcePath = "MultiplayerCore.UI.MpCoreGameplaySetup.bsml"; - - private GameplaySetupViewController _gameplaySetup; - private MultiplayerSettingsPanelController _multiplayerSettingsPanel; - private MainFlowCoordinator _mainFlowCoordinator; - private MpCoreSetupFlowCoordinator _setupFlowCoordinator; - //private readonly Config _config; - private SiraLog _logger; - private bool _perPlayerDiffs = false; - private bool _perPlayerModifiers = false; - - internal MpCoreGameplaySetup( - GameplaySetupViewController gameplaySetup, - MainFlowCoordinator mainFlowCoordinator, - MpCoreSetupFlowCoordinator setupFlowCoordinator, - //Config config, - SiraLog logger) - { - _gameplaySetup = gameplaySetup; - _multiplayerSettingsPanel = gameplaySetup._multiplayerSettingsPanelController; - _mainFlowCoordinator = mainFlowCoordinator; - _setupFlowCoordinator = setupFlowCoordinator; - //_config = config; - _logger = logger; - } - - public void Initialize() - { - BSMLParser.instance.Parse(BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _multiplayerSettingsPanel.gameObject, this); - while (0 < _vert.transform.childCount) - _vert.transform.GetChild(0).SetParent(_multiplayerSettingsPanel.transform); - } - - public void Dispose() - { - - } - - [UIAction("#post-parse")] - private void PostParse() - { - - } - - - [UIAction("preferences-click")] - private void PresentPreferences() - { - FlowCoordinator deepestChildFlowCoordinator = DeepestChildFlowCoordinator(_mainFlowCoordinator); - _setupFlowCoordinator.parentFlowCoordinator = deepestChildFlowCoordinator; - deepestChildFlowCoordinator.PresentFlowCoordinator(_setupFlowCoordinator); - } - - private FlowCoordinator DeepestChildFlowCoordinator(FlowCoordinator root) - { - var flow = root.childFlowCoordinator; - if (flow == null) return root; - if (flow.childFlowCoordinator == null || flow.childFlowCoordinator == flow) - { - return flow; - } - return DeepestChildFlowCoordinator(flow); - } - - [UIObject("vert")] - private GameObject _vert = null!; - - [UIValue("per-player-diffs")] - public bool PerPlayerDifficulty - { - get => _perPlayerDiffs; - set - { - _perPlayerDiffs = value; - NotifyPropertyChanged(); - } - } - - [UIValue("per-player-modifiers")] - public bool PerPlayerModifiers - { - get => _perPlayerModifiers; - set - { - _perPlayerModifiers = value; - NotifyPropertyChanged(); - } - } - } -} diff --git a/MultiplayerCore/UI/MpCoreSetupFlowCoordinator.cs b/MultiplayerCore/UI/MpCoreSetupFlowCoordinator.cs deleted file mode 100644 index f6c8e0d..0000000 --- a/MultiplayerCore/UI/MpCoreSetupFlowCoordinator.cs +++ /dev/null @@ -1,62 +0,0 @@ -using HMUI; -using Zenject; -using BeatSaberMarkupLanguage; -using SiraUtil.Affinity; -using System; - -namespace MultiplayerCore.UI -{ - public class MpCoreSetupFlowCoordinator : FlowCoordinator - { - internal FlowCoordinator parentFlowCoordinator = null!; - //private MpexSettingsViewController _settingsViewController = null!; - //private MpexEnvironmentViewController _environmentViewController = null!; - //private MpexMiscViewController _miscViewController = null!; - private ILobbyGameStateController _gameStateController = null!; - - [Inject] - public void Construct( - MainFlowCoordinator mainFlowCoordinator, - //MpexSettingsViewController settingsViewController, - //MpexEnvironmentViewController environmentViewController, - //MpexMiscViewController miscViewController, - ILobbyGameStateController gameStateController) - { - parentFlowCoordinator = mainFlowCoordinator; - //_settingsViewController = settingsViewController; - //_environmentViewController = environmentViewController; - //_miscViewController = miscViewController; - _gameStateController = gameStateController; - } - - protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) - { - if (firstActivation) - { - SetTitle("Lobby Preferences"); - showBackButton = true; - } - if (addedToHierarchy) - { - //ProvideInitialViewControllers(_settingsViewController, _environmentViewController, _miscViewController); - _gameStateController.gameStartedEvent += DismissGameStartedEvent; - } - } - - protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) - { - if (removedFromHierarchy) - _gameStateController.gameStartedEvent -= DismissGameStartedEvent; - } - - private void DismissGameStartedEvent(ILevelGameplaySetupData obj) - { - parentFlowCoordinator.DismissFlowCoordinator(this, null, ViewController.AnimationDirection.Horizontal, true); - } - - protected override void BackButtonWasPressed(ViewController topViewController) - { - parentFlowCoordinator.DismissFlowCoordinator(this); - } - } -} From 2a87538003eec57019007ab01c5b1743ac3860d5 Mon Sep 17 00:00:00 2001 From: ehlor <38947148+ehlor@users.noreply.github.com> Date: Sun, 25 Aug 2024 20:23:07 +0300 Subject: [PATCH 69/75] add separate folder for mp songs (#39) Co-authored-by: Michael R. --- MultiplayerCore/Icons/MpFolder.png | Bin 0 -> 83485 bytes MultiplayerCore/MultiplayerCore.csproj | 1 + MultiplayerCore/Plugin.cs | 7 ++++++- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 MultiplayerCore/Icons/MpFolder.png diff --git a/MultiplayerCore/Icons/MpFolder.png b/MultiplayerCore/Icons/MpFolder.png new file mode 100644 index 0000000000000000000000000000000000000000..eba5788ca9b9745fdb446ac42ad609ee9768dcec GIT binary patch literal 83485 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

qVUN(#$ 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 literal 0 HcmV?d00001 diff --git a/MultiplayerCore/MultiplayerCore.csproj b/MultiplayerCore/MultiplayerCore.csproj index 6cc1edd..1b70558 100644 --- a/MultiplayerCore/MultiplayerCore.csproj +++ b/MultiplayerCore/MultiplayerCore.csproj @@ -244,6 +244,7 @@ + diff --git a/MultiplayerCore/Plugin.cs b/MultiplayerCore/Plugin.cs index 0aab2b6..1cd2bb2 100644 --- a/MultiplayerCore/Plugin.cs +++ b/MultiplayerCore/Plugin.cs @@ -42,7 +42,12 @@ public Plugin(IPALogger logger, PluginMetadata pluginMetadata, Zenjector zenject [OnEnable] public void OnEnable() { - SongCore.Collections.AddSeparateSongFolder("Multiplayer", Path.Combine(Application.dataPath, CustomLevelsPath), SongCore.Data.FolderLevelPack.CustomLevels); + SongCore.Collections.AddSeperateSongFolder( + "Multiplayer", + Path.Combine(Application.dataPath, CustomLevelsPath), + SongCore.Data.FolderLevelPack.NewPack, + SongCore.Utilities.Utils.LoadSpriteFromResources("MultiplayerCore.Icons.MpFolder.png") + ); _harmony.PatchAll(_metadata.Assembly); } From 572c17bf2da72d6db7606c425f946a64d7c10046 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 25 Aug 2024 19:24:39 +0200 Subject: [PATCH 70/75] Fix typo --- MultiplayerCore/Plugin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiplayerCore/Plugin.cs b/MultiplayerCore/Plugin.cs index 1cd2bb2..ad9891b 100644 --- a/MultiplayerCore/Plugin.cs +++ b/MultiplayerCore/Plugin.cs @@ -42,7 +42,7 @@ public Plugin(IPALogger logger, PluginMetadata pluginMetadata, Zenjector zenject [OnEnable] public void OnEnable() { - SongCore.Collections.AddSeperateSongFolder( + SongCore.Collections.AddSeparateSongFolder( "Multiplayer", Path.Combine(Application.dataPath, CustomLevelsPath), SongCore.Data.FolderLevelPack.NewPack, From 00ea0c0bc7bd227d062706049e23ae56b0843d89 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Sun, 25 Aug 2024 19:25:28 +0200 Subject: [PATCH 71/75] Remove deleted reference --- MultiplayerCore/MultiplayerCore.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/MultiplayerCore/MultiplayerCore.csproj b/MultiplayerCore/MultiplayerCore.csproj index 1b70558..45aadb5 100644 --- a/MultiplayerCore/MultiplayerCore.csproj +++ b/MultiplayerCore/MultiplayerCore.csproj @@ -247,9 +247,6 @@ - - Never - Never From e2905e5772f5b3696dfbce5198ab9a2cfc52cfa7 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Mon, 26 Aug 2024 19:33:19 +0200 Subject: [PATCH 72/75] Re-fix ordering from #51 Allow sending beatmap to single player (newly joined) Co-authored-by: rcelyte --- MultiplayerCore/Objects/MpPlayersDataModel.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index f969062..fc6622c 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; @@ -58,7 +59,7 @@ internal void HandlePlayerConnected(IConnectedPlayer connectedPlayer) { // Send our MpBeatmapPacket again so newly joined players have it var selectedBeatmapKey = _playersData[localUserId].beatmapKey; - SendMpBeatmapPacket(selectedBeatmapKey); + SendMpBeatmapPacket(selectedBeatmapKey, connectedPlayer); } internal void SetLocalPlayerBeatmapLevel_override(in BeatmapKey beatmapKey) { @@ -104,7 +105,7 @@ private void HandleMpCoreBeatmapPacket(MpBeatmapPacket packet, IConnectedPlayer base.HandleMenuRpcManagerRecommendBeatmap(userId, beatmapKeySerializable); } - private async void SendMpBeatmapPacket(BeatmapKey beatmapKey) + private void SendMpBeatmapPacket(BeatmapKey beatmapKey, IConnectedPlayer? player = null) { var levelId = beatmapKey.levelId; _logger.Debug($"Sending beatmap packet for level {levelId}"); @@ -116,16 +117,18 @@ private async void SendMpBeatmapPacket(BeatmapKey beatmapKey) return; } - var levelData = await _beatmapLevelProvider.GetBeatmap(levelHash); - if (levelData == null) + var levelData = _beatmapLevelProvider.GetBeatmapFromLocalBeatmaps(levelHash); + var packet = (levelData != null) ? new MpBeatmapPacket(levelData, beatmapKey) : FindLevelPacket(levelHash); + if (packet == null) { - _logger.Debug("Could not get level data for beatmap, returning!"); + _logger.Warn($"Could not get level data for beatmap '{levelHash}', returning!"); return; } - var packet = new MpBeatmapPacket(levelData, beatmapKey); - _logger.Debug("Actually sending packet"); - _multiplayerSessionManager.Send(packet); + if (player != null) + _multiplayerSessionManager.SendToPlayer(packet, player); + else + _multiplayerSessionManager.Send(packet); } public MpBeatmapPacket? GetPlayerPacket(string playerId) From f4d8ce6277bc3762036ddb578c0f03d2e898c795 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Mon, 26 Aug 2024 19:34:05 +0200 Subject: [PATCH 73/75] On player connected, check if selected map is valid --- MultiplayerCore/Objects/MpPlayersDataModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiplayerCore/Objects/MpPlayersDataModel.cs b/MultiplayerCore/Objects/MpPlayersDataModel.cs index fc6622c..c71aff3 100644 --- a/MultiplayerCore/Objects/MpPlayersDataModel.cs +++ b/MultiplayerCore/Objects/MpPlayersDataModel.cs @@ -59,8 +59,8 @@ internal void HandlePlayerConnected(IConnectedPlayer connectedPlayer) { // Send our MpBeatmapPacket again so newly joined players have it var selectedBeatmapKey = _playersData[localUserId].beatmapKey; - SendMpBeatmapPacket(selectedBeatmapKey, connectedPlayer); - } + if (selectedBeatmapKey.IsValid()) SendMpBeatmapPacket(selectedBeatmapKey, connectedPlayer); + } internal void SetLocalPlayerBeatmapLevel_override(in BeatmapKey beatmapKey) { // Game: The local player has selected / recommended a beatmap From 082e08fb2ed3ca46ec8a0e1a849a2c571a42ce37 Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Mon, 26 Aug 2024 23:00:34 +0200 Subject: [PATCH 74/75] Fixes to map selection --- .../Patches/NoLevelSpectatorPatch.cs | 36 ++++++++++++++----- MultiplayerCore/UI/MpPerPlayerUI.cs | 16 +++++++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs b/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs index df5aa9b..f347593 100644 --- a/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs +++ b/MultiplayerCore/Patches/NoLevelSpectatorPatch.cs @@ -8,6 +8,7 @@ using HarmonyLib; using IPA.Utilities; using MultiplayerCore.Beatmaps; +using MultiplayerCore.Beatmaps.Abstractions; using MultiplayerCore.Beatmaps.Providers; using MultiplayerCore.Objects; using MultiplayerCore.Patchers; @@ -17,19 +18,34 @@ namespace MultiplayerCore.Patches [HarmonyPatch] internal class NoLevelSpectatorPatch { - internal static MpBeatmapLevelProvider? mpBeatmapLevelProvider; + internal static MpBeatmapLevelProvider? _mpBeatmapLevelProvider; + internal static MpPlayersDataModel? _playersDataModel; [HarmonyPrefix] [HarmonyPatch(typeof(LobbyGameStateController), nameof(LobbyGameStateController.StartMultiplayerLevel))] internal static bool LobbyGameStateController_StartMultiplayerLevel(LobbyGameStateController __instance, ILevelGameplaySetupData gameplaySetupData, IBeatmapLevelData beatmapLevelData, Action beforeSceneSwitchCallback) { - mpBeatmapLevelProvider = ((MpPlayersDataModel)__instance._lobbyPlayersDataModel)._beatmapLevelProvider; + _playersDataModel = __instance._lobbyPlayersDataModel as MpPlayersDataModel; + _mpBeatmapLevelProvider = _playersDataModel?._beatmapLevelProvider; + + if (_playersDataModel == null || _mpBeatmapLevelProvider == null) + { + Plugin.Logger.Critical($"Missing custom MpPlayersDataModel or MpBeatmapLevelProvider, cannot continue, returning..."); + return false; + } var levelHash = Utilities.HashForLevelID(gameplaySetupData.beatmapKey.levelId); if (gameplaySetupData != null && beatmapLevelData == null && !string.IsNullOrWhiteSpace(levelHash)) { Plugin.Logger.Info($"No LevelData for custom level {levelHash} running patch for spectator"); - var levelTask = mpBeatmapLevelProvider.GetBeatmap(levelHash); + var packet = _playersDataModel.FindLevelPacket(levelHash); + Task? levelTask = null; + if (packet != null) + { + levelTask = Task.FromResult(_mpBeatmapLevelProvider.GetBeatmapFromPacket(packet)); + } + + if (levelTask == null) levelTask = _mpBeatmapLevelProvider.GetBeatmap(levelHash); __instance.countdownStarted = false; __instance.StopListeningToGameStart(); // Ensure we stop listening for the start event while we run our start task levelTask.ContinueWith(beatmapTask => @@ -37,10 +53,10 @@ internal static bool LobbyGameStateController_StartMultiplayerLevel(LobbyGameSta if (__instance.countdownStarted) return; // Another countdown has started, don't start the level BeatmapLevel? beatmapLevel = beatmapTask.Result?.MakeBeatmapLevel(gameplaySetupData.beatmapKey, - mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(levelHash)); + _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(levelHash)); if (beatmapLevel == null) beatmapLevel = new NoInfoBeatmapLevel(levelHash).MakeBeatmapLevel(gameplaySetupData.beatmapKey, - mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(levelHash)); + _mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(levelHash)); __instance._menuTransitionsHelper.StartMultiplayerLevel("Multiplayer", gameplaySetupData.beatmapKey, beatmapLevel, beatmapLevelData, __instance._playerDataModel.playerData.colorSchemesSettings.GetOverrideColorScheme(), gameplaySetupData.gameplayModifiers, @@ -82,14 +98,18 @@ static bool Prepare() internal static void MultiplayerResultsViewController_Init(MultiplayerResultsViewController __instance, BeatmapKey beatmapKey) { var hash = Utilities.HashForLevelID(beatmapKey.levelId); - if (NoLevelSpectatorPatch.mpBeatmapLevelProvider != null && !string.IsNullOrWhiteSpace(hash) && + if (NoLevelSpectatorPatch._mpBeatmapLevelProvider != null && !string.IsNullOrWhiteSpace(hash) && SongCore.Loader.GetLevelByHash(hash) == null) { IPA.Utilities.Async.UnityMainThreadTaskScheduler.Factory.StartNew(async () => { - BeatmapLevel? beatmapLevel = (await NoLevelSpectatorPatch.mpBeatmapLevelProvider.GetBeatmap(hash))?.MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch.mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); + var packet = NoLevelSpectatorPatch._playersDataModel?.FindLevelPacket(hash); + BeatmapLevel? beatmapLevel = packet != null ? NoLevelSpectatorPatch._mpBeatmapLevelProvider.GetBeatmapFromPacket(packet)? + .MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch._mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)) : null; + if (beatmapLevel == null) beatmapLevel = (await NoLevelSpectatorPatch._mpBeatmapLevelProvider.GetBeatmap(hash))? + .MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch._mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); if (beatmapLevel == null) - beatmapLevel = new NoInfoBeatmapLevel(hash).MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch.mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); + beatmapLevel = new NoInfoBeatmapLevel(hash).MakeBeatmapLevel(beatmapKey, NoLevelSpectatorPatch._mpBeatmapLevelProvider.MakeBeatSaverPreviewMediaData(hash)); Plugin.Logger.Trace($"Calling Setup with level type: {beatmapLevel.GetType().Name}, beatmapCharacteristic type: {beatmapKey.beatmapCharacteristic.GetType().Name}, difficulty type: {beatmapKey.difficulty.GetType().Name} "); if (_newlbarInfo) { diff --git a/MultiplayerCore/UI/MpPerPlayerUI.cs b/MultiplayerCore/UI/MpPerPlayerUI.cs index afc9df3..7d15732 100644 --- a/MultiplayerCore/UI/MpPerPlayerUI.cs +++ b/MultiplayerCore/UI/MpPerPlayerUI.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Reflection; using MultiplayerCore.Models; +using MultiplayerCore.Objects; using MultiplayerCore.Repositories; using UnityEngine; using UnityEngine.UI; @@ -31,6 +32,7 @@ internal class MpPerPlayerUI : IInitializable, IDisposable private readonly BeatmapLevelsModel _beatmapLevelsModel; private readonly MpPacketSerializer _packetSerializer; private readonly MpBeatmapLevelProvider _beatmapLevelProvider; + private readonly MpPlayersDataModel _playersDataModel; private readonly MpStatusRepository _statusRepository; private readonly NetworkConfigPatcher _networkConfig; private BeatmapKey _currentBeatmapKey; @@ -56,6 +58,7 @@ public MpPerPlayerUI( _beatmapLevelsModel = beatmapLevelsModel; _multiplayerSessionManager = sessionManager; _beatmapLevelProvider = beatmapLevelProvider; + _playersDataModel = _gameServerLobbyFlowCoordinator._lobbyPlayersDataModel as MpPlayersDataModel; _packetSerializer = packetSerializer; _statusRepository = statusRepository; _networkConfig = networkConfig; @@ -258,11 +261,17 @@ private void UpdateDifficultyListWithBeatmapKey(BeatmapKey beatmapKey) { _logger.Debug( $"Level {levelHash} has empty requirements, this should not happen, falling back to packet"); - level = _beatmapLevelProvider.TryGetBeatmapFromPacketHash(levelHash); - if (level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Count > 0) + //level = _beatmapLevelProvider.TryGetBeatmapFromPacketHash(levelHash); + var packet = _playersDataModel.FindLevelPacket(levelHash); + level = packet != null ? _beatmapLevelProvider.GetBeatmapFromPacket(packet) : null; + if (level != null && level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Count > 0) UpdateDifficultyList(level.Requirements[beatmapKey.beatmapCharacteristic.serializedName].Keys .ToList()); - else _logger.Debug($"Level packet {levelHash} also has empty requirements, this should not happen..."); + else + { + _logger.Debug($"Level packet {levelHash} also has empty requirements, this should not happen..."); + UpdateDifficultyList(new [] {beatmapKey.difficulty}); + } } } else _logger.Error($"Failed to get level for hash {levelHash}"); @@ -275,6 +284,7 @@ private void UpdateDifficultyListWithBeatmapKey(BeatmapKey beatmapKey) ?.GetDifficulties(beatmapKey.beatmapCharacteristic).ToList(); if (diffList != null) UpdateDifficultyList(diffList); } + } private void UpdateDifficultyList(IReadOnlyList difficulties) From 5cc76956ed4ed6b810f9f085a2d70641f998703e Mon Sep 17 00:00:00 2001 From: EnderdracheLP Date: Wed, 28 Aug 2024 21:57:36 +0200 Subject: [PATCH 75/75] Disable sending MpPlayerData again, I forgot that it's sent from the server not the client --- MultiplayerCore/Players/MpPlayerManager.cs | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/MultiplayerCore/Players/MpPlayerManager.cs b/MultiplayerCore/Players/MpPlayerManager.cs index 473ec15..69a95bd 100644 --- a/MultiplayerCore/Players/MpPlayerManager.cs +++ b/MultiplayerCore/Players/MpPlayerManager.cs @@ -34,7 +34,7 @@ public async void Initialize() { _sessionManager.SetLocalPlayerState("modded", true); _packetSerializer.RegisterCallback(HandlePlayerData); - _sessionManager.playerConnectedEvent += HandlePlayerConnected; + //_sessionManager.playerConnectedEvent += HandlePlayerConnected; _localPlayerInfo = await _platformUserModel.GetUserInfo(CancellationToken.None); } @@ -44,22 +44,22 @@ public void Dispose() _packetSerializer.UnregisterCallback(); } - private void HandlePlayerConnected(IConnectedPlayer player) - { - if (_localPlayerInfo == null) - throw new NullReferenceException("local player info was not yet set! make sure it is set before anything else happens!"); + //private void HandlePlayerConnected(IConnectedPlayer player) + //{ + // if (_localPlayerInfo == null) + // throw new NullReferenceException("local player info was not yet set! make sure it is set before anything else happens!"); - _sessionManager.Send(new MpPlayerData - { - Platform = _localPlayerInfo.platform switch - { - UserInfo.Platform.Oculus => Platform.OculusPC, - UserInfo.Platform.Steam => Platform.Steam, - _ => Platform.Unknown - }, - PlatformId = _localPlayerInfo.platformUserId - }); - } + // _sessionManager.Send(new MpPlayerData + // { + // Platform = _localPlayerInfo.platform switch + // { + // UserInfo.Platform.Oculus => Platform.OculusPC, + // UserInfo.Platform.Steam => Platform.Steam, + // _ => Platform.Unknown + // }, + // PlatformId = _localPlayerInfo.platformUserId + // }); + //} private void HandlePlayerData(MpPlayerData packet, IConnectedPlayer player) {