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