diff --git a/CHANGELOG.md b/CHANGELOG.md index 36638ac..4a69640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## MuzikaGromche 1337.69.420 - Fix certain object hanging around after being disabled. +- CSync proved to be unreliable for config syncing, so rewrote track selection to custom netcode. ## MuzikaGromche 13.37.9001 - Chromaberrated Edition diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..1a9b767 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/MuzikaGromche/MuzikaGromche.csproj b/MuzikaGromche/MuzikaGromche.csproj index 8b303f0..d7bd3f4 100644 --- a/MuzikaGromche/MuzikaGromche.csproj +++ b/MuzikaGromche/MuzikaGromche.csproj @@ -13,6 +13,9 @@ latest enable + + portable + ../README.md https://git.vilunov.me/ratijas/muzika-gromche https://git.vilunov.me/ratijas/muzika-gromche diff --git a/MuzikaGromche/Plugin.cs b/MuzikaGromche/Plugin.cs index 6f4adbf..45dcddc 100644 --- a/MuzikaGromche/Plugin.cs +++ b/MuzikaGromche/Plugin.cs @@ -9,6 +9,7 @@ using LethalConfig.ConfigItems.Options; using LobbyCompatibility.Attributes; using LobbyCompatibility.Enums; using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -16,6 +17,7 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Reflection; using System.Security.Cryptography; +using Unity.Netcode; using UnityEngine; using UnityEngine.Networking; @@ -482,6 +484,11 @@ namespace MuzikaGromche return tracks[trackId]; } + public static Track? FindTrackNamed(string name) + { + return Tracks.FirstOrDefault(track => track.Name == name); + } + internal static Track? CurrentTrack; internal static BeatTimeState? BeatTimeState; @@ -547,6 +554,7 @@ namespace MuzikaGromche DiscoBallManager.Load(); PoweredLightsAnimators.Load(); var harmony = new Harmony(PluginInfo.PLUGIN_NAME); + harmony.PatchAll(typeof(GameNetworkManagerPatch)); harmony.PatchAll(typeof(JesterPatch)); harmony.PatchAll(typeof(EnemyAIPatch)); harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch)); @@ -554,6 +562,7 @@ namespace MuzikaGromche harmony.PatchAll(typeof(DiscoBallTilePatch)); harmony.PatchAll(typeof(DiscoBallDespawnPatch)); harmony.PatchAll(typeof(SpawnRatePatch)); + NetcodePatcher(); } else { @@ -561,6 +570,23 @@ namespace MuzikaGromche Logger.LogError("Could not load audio file " + string.Join(", ", failed)); } } + + private static void NetcodePatcher() + { + var types = Assembly.GetExecutingAssembly().GetTypes(); + foreach (var type in types) + { + var methods = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + foreach (var method in methods) + { + var attributes = method.GetCustomAttributes(typeof(RuntimeInitializeOnLoadMethodAttribute), false); + if (attributes.Length > 0) + { + method.Invoke(null, null); + } + } + } + } }; public readonly record struct Language(string Short, string Full) @@ -680,7 +706,7 @@ namespace MuzikaGromche public AudioClip LoadedLoop = null!; // How often this track should be chosen, relative to the sum of weights of all tracks. - public SyncedEntry Weight = null!; + public ConfigEntry Weight = null!; public string FileNameStart => $"{Name}Start.{Ext}"; public string FileNameLoop => $"{Name}Loop.{Ext}"; @@ -1428,7 +1454,7 @@ namespace MuzikaGromche public static ConfigEntry SkipExplicitTracks { get; private set; } = null!; - public static SyncedEntry OverrideSpawnRates { get; private set; } = null!; + public static ConfigEntry OverrideSpawnRates { get; private set; } = null!; public static bool ShouldSkipWindingPhase { get; private set; } = false; @@ -1456,12 +1482,11 @@ namespace MuzikaGromche SkipExplicitTracks = configFile.Bind("General", "Skip Explicit Tracks", false, new ConfigDescription("When choosing tracks at random, skip the ones with Explicit Content/Lyrics.")); - LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(SkipExplicitTracks, requiresRestart: false)); + LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(SkipExplicitTracks, Default(new BoolCheckBoxOptions()))); - OverrideSpawnRates = configFile.BindSyncedEntry("General", "Override Spawn Rates", false, + OverrideSpawnRates = configFile.Bind("General", "Override Spawn Rates", false, new ConfigDescription("Deviate from vanilla spawn rates to experience content of this mod more often.")); - LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates.Entry, Default(new BoolCheckBoxOptions()))); - CSyncHackAddSyncedEntry(OverrideSpawnRates); + LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions()))); #if DEBUG SetupEntriesToSkipWinding(configFile); @@ -1489,11 +1514,11 @@ namespace MuzikaGromche if (CanModifyWeightsNow()) { var tracks = Plugin.Tracks.Where(t => t.Language.Equals(language)).ToList(); - var isOff = tracks.All(t => t.Weight.LocalValue == 0); + var isOff = tracks.All(t => t.Weight.Value == 0); var newWeight = isOff ? 50 : 0; foreach (var t in tracks) { - t.Weight.LocalValue = newWeight; + t.Weight.Value = newWeight; } } }); @@ -1504,13 +1529,12 @@ namespace MuzikaGromche // Create slider entry for track string warning = track.IsExplicit ? "Explicit Content/Lyrics!\n\n" : ""; string description = $"Language: {language.Full}\n\n{warning}Random (relative) chance of selecting this track.\n\nSet to zero to effectively disable the track."; - track.Weight = configFile.BindSyncedEntry( + track.Weight = configFile.Bind( new ConfigDefinition(section, track.Name), 50, new ConfigDescription(description, chanceRange, track)); - LethalConfigManager.AddConfigItem(new IntSliderConfigItem(track.Weight.Entry, Default(new IntSliderOptions()))); - CSyncHackAddSyncedEntry(track.Weight); + LethalConfigManager.AddConfigItem(new IntSliderConfigItem(track.Weight, Default(new IntSliderOptions()))); } ConfigManager.Register(this); @@ -1774,6 +1798,78 @@ namespace MuzikaGromche } } + [HarmonyPatch(typeof(GameNetworkManager))] + static class GameNetworkManagerPatch + { + const string JesterEnemyPrefabName = "JesterEnemy"; + + [HarmonyPatch(nameof(GameNetworkManager.Start))] + [HarmonyPrefix] + static void StartPrefix(GameNetworkManager __instance) + { + var networkPrefab = NetworkManager.Singleton.NetworkConfig.Prefabs.Prefabs + .FirstOrDefault(prefab => prefab.Prefab.name == JesterEnemyPrefabName); + if (networkPrefab == null) + { + Debug.LogError($"{nameof(MuzikaGromche)} JesterEnemy prefab not found!"); + } + else + { + Debug.Log($"{nameof(MuzikaGromche)} Patching {nameof(JesterAI)} with {nameof(MuzikaGromcheJesterNetworkBehaviour)} component"); + networkPrefab.Prefab.AddComponent(); + } + } + } + + class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour + { + public override void OnNetworkSpawn() + { + ChooseTrackDeferred(); + foreach (var track in Plugin.Tracks) + { + track.Weight.SettingChanged += (_, _) => ChooseTrackDeferred(); + } + Config.SkipExplicitTracks.SettingChanged += (_, _) => ChooseTrackDeferred(); + base.OnNetworkSpawn(); + } + + // Batch multiple weights changes in a single network RPC + private Coroutine? DeferredCoroutine = null; + + private void ChooseTrackDeferred() + { + if (DeferredCoroutine != null) + { + StopCoroutine(DeferredCoroutine); + DeferredCoroutine = null; + } + DeferredCoroutine = StartCoroutine(ChooseTrackDeferredCoroutine()); + } + + private IEnumerator ChooseTrackDeferredCoroutine() + { + yield return new WaitForEndOfFrame(); + DeferredCoroutine = null; + ChooseTrackServerRpc(); + } + + [ClientRpc] + public void SetTrackClientRpc(string name) + { + Debug.Log($"{nameof(MuzikaGromche)} SetTrackClientRpc {name}"); + Plugin.CurrentTrack = Plugin.FindTrackNamed(name); + } + + [ServerRpc] + public void ChooseTrackServerRpc() + { + var track = Plugin.ChooseTrack(); + Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {track.Name}"); + SetTrackClientRpc(track.Name); + } + } + // farAudio is during windup, Start overrides popGoesTheWeaselTheme // creatureVoice is when popped, Loop overrides screamingSFX [HarmonyPatch(typeof(JesterAI))] @@ -1820,6 +1916,14 @@ namespace MuzikaGromche [HarmonyPostfix] static void JesterUpdatePostfix(JesterAI __instance, State __state) { + if (Plugin.CurrentTrack == null) + { +#if DEBUG + Debug.Log($"{nameof(MuzikaGromche)} CurrentTrack is not set!"); +#endif + return; + } + if (__instance.previousState == 1 && __state.previousState != 1) { // if just started winding up @@ -1828,7 +1932,6 @@ namespace MuzikaGromche __instance.creatureVoice.Stop(); // ...and start modded music - Plugin.CurrentTrack = Plugin.ChooseTrack(); Plugin.BeatTimeState = new BeatTimeState(Plugin.CurrentTrack); // Set up custom popup timer, which is shorter than Start audio __instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer; @@ -1878,9 +1981,9 @@ namespace MuzikaGromche } // Manage the timeline: switch color of the lights according to the current playback/beat position. - if (__instance.previousState == 1 || __instance.previousState == 2) + if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState != null) { - var events = Plugin.BeatTimeState!.Update(start: __instance.farAudio, loop: __instance.creatureVoice); + var events = Plugin.BeatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice); foreach (var ev in events) { switch (ev) diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..a67e057 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "evaisa.netcodepatcher.cli": { + "version": "4.3.0", + "commands": [ + "netcode-patch" + ] + } + } +} \ No newline at end of file