forked from nikita/muzika-gromche
				
			Compare commits
	
		
			No commits in common. "d02e594457e772f83b798a6ee84a32469cbdc398" and "c7b67b9042bb185d5ac6d8f7670673e4aed5c2d9" have entirely different histories.
		
	
	
		
			d02e594457
			...
			c7b67b9042
		
	
		|  | @ -2,9 +2,6 @@ | |||
| 
 | ||||
| ## MuzikaGromche 13.37.9001 | ||||
| 
 | ||||
| - Fixed more missing flickering behaviours for some animators controllers. | ||||
| - Fixed some powered lights not fully turning off or flickering when there are multiple Light components per container. | ||||
| - Improved performance by pre-loading certain assets at the start of round instead of at a timing-critical frame update. | ||||
| 
 | ||||
| ## MuzikaGromche 13.37.1337 - Photosensitivity Warning Edition | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,4 @@ | |||
| using DunGen; | ||||
| using HarmonyLib; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
|  | @ -9,21 +7,54 @@ using UnityEngine; | |||
| 
 | ||||
| namespace MuzikaGromche | ||||
| { | ||||
|     public static class DiscoBallManager | ||||
|     public class DiscoBallManager : MonoBehaviour | ||||
|     { | ||||
|         // A struct holding a disco ball container object and the name of a tile for which it was designed. | ||||
|         private readonly record struct TilePatch(string TileName, GameObject DiscoBallContainer) | ||||
|         public readonly record struct Data(string TileName, GameObject DiscoBallContainer) | ||||
|         { | ||||
|             // We are specifically looking for cloned tiles, not the original prototypes. | ||||
|             public readonly string TileCloneName = $"{TileName}(Clone)"; | ||||
|         } | ||||
| 
 | ||||
|         private static TilePatch[] Patches = []; | ||||
|         public static readonly List<Data> Containers = []; | ||||
|         private static readonly List<GameObject> InstantiatedContainers = []; | ||||
| 
 | ||||
|         private static readonly List<GameObject> CachedDiscoBalls = []; | ||||
|         private static readonly List<Animator> CachedDiscoBallAnimators = []; | ||||
|         public static void Initialize() | ||||
|         { | ||||
|             string bundlePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "muzikagromche_discoball"); | ||||
|             var bundle = AssetBundle.LoadFromFile(bundlePath); | ||||
| 
 | ||||
|         private static readonly string[] AnimatorContainersNames = [ | ||||
|             foreach ((string prefabPath, string tileName) in new[] { | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManor.prefab", "ManorStartRoomSmall"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManorOLD.prefab", "ManorStartRoom"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerFactory.prefab", "StartRoom"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerMineShaft.prefab", "MineshaftStartTile"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerLargeForkTileB.prefab", "LargeForkTileB"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerBirthdayRoomTile.prefab", "BirthdayRoomTile"), | ||||
|             }) | ||||
|             { | ||||
|                 var container = bundle.LoadAsset<GameObject>(prefabPath); | ||||
|                 Containers.Add(new(tileName, container)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public static void Enable() | ||||
|         { | ||||
|             // Just in case | ||||
|             Disable(); | ||||
| 
 | ||||
|             var query = from tile in Resources.FindObjectsOfTypeAll<Tile>() | ||||
|                         join container in Containers | ||||
|                         on tile.gameObject.name equals container.TileCloneName | ||||
|                         select (tile, container); | ||||
| 
 | ||||
|             foreach (var (tile, container) in query) | ||||
|             { | ||||
|                 Enable(tile, container); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private static readonly string[] animatorNames = [ | ||||
|             "DiscoBallProp/AnimContainer", | ||||
|             "DiscoBallProp1/AnimContainer", | ||||
|             "DiscoBallProp2/AnimContainer", | ||||
|  | @ -32,134 +63,29 @@ namespace MuzikaGromche | |||
|             "DiscoBallProp5/AnimContainer", | ||||
|         ]; | ||||
| 
 | ||||
|         public static void Load() | ||||
|         private static void Enable(Tile tile, Data container) | ||||
|         { | ||||
|             const string BundleFileName = "muzikagromche_discoball"; | ||||
|             string bundlePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), BundleFileName); | ||||
|             var assetBundle = AssetBundle.LoadFromFile(bundlePath) | ||||
|                 ?? throw new NullReferenceException("Failed to load bundle"); | ||||
|             Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Enabling at '{tile.gameObject.name}'"); | ||||
|             var discoBall = Instantiate(container.DiscoBallContainer, tile.transform); | ||||
|             InstantiatedContainers.Add(discoBall); | ||||
| 
 | ||||
|             (string PrefabPath, string TileName)[] patchDescriptors = | ||||
|             [ | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManor.prefab", "ManorStartRoomSmall"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManorOLD.prefab", "ManorStartRoom"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerFactory.prefab", "StartRoom"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerMineShaft.prefab", "MineshaftStartTile"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerLargeForkTileB.prefab", "LargeForkTileB"), | ||||
|                 ("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerBirthdayRoomTile.prefab", "BirthdayRoomTile"), | ||||
|             ]; | ||||
| 
 | ||||
|             Patches = [.. patchDescriptors.Select(d => | ||||
|                 new TilePatch(d.TileName, assetBundle.LoadAsset<GameObject>(d.PrefabPath)) | ||||
|             )]; | ||||
|         } | ||||
| 
 | ||||
|         internal static void Patch(Tile tile) | ||||
|             foreach (var animatorName in animatorNames) | ||||
|             { | ||||
|             var query = from patch in Patches | ||||
|                         where tile.gameObject.name == patch.TileCloneName | ||||
|                         select patch; | ||||
| 
 | ||||
|             // Should be just one, but FirstOrDefault() isn't usable with structs | ||||
|             foreach (var patch in query) | ||||
|                 if (discoBall.transform.Find(animatorName)?.gameObject is GameObject animator) | ||||
|                 { | ||||
|                 Patch(tile, patch); | ||||
|                     animator.GetComponent<Animator>().SetBool("on", true); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         static void Patch(Tile tile, TilePatch patch) | ||||
|         { | ||||
|             var discoBall = UnityEngine.Object.Instantiate(patch.DiscoBallContainer, tile.transform); | ||||
|             if (discoBall == null) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             foreach (var animator in FindDiscoBallAnimators(discoBall)) | ||||
|             { | ||||
|                 CachedDiscoBallAnimators.Add(animator); | ||||
|             } | ||||
|             CachedDiscoBalls.Add(discoBall); | ||||
|             discoBall.SetActive(false); | ||||
| 
 | ||||
|             Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Patched tile '{tile.gameObject.name}'"); | ||||
|         } | ||||
| 
 | ||||
|         static IEnumerable<Animator> FindDiscoBallAnimators(GameObject discoBall) | ||||
|         { | ||||
|             foreach (var animatorContainerName in AnimatorContainersNames) | ||||
|             { | ||||
|                 var transform = discoBall.transform.Find(animatorContainerName); | ||||
|                 if (transform == null) | ||||
|                 { | ||||
|                     // Not all prefabs have all possible animators, and it's OK | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 var animator = transform.gameObject?.GetComponent<Animator>(); | ||||
|                 if (animator == null) | ||||
|                 { | ||||
|                     // This would be weird | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 yield return animator; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public static void Toggle(bool on) | ||||
|         { | ||||
|             Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Toggle {(on ? "ON" : "OFF")} {CachedDiscoBallAnimators.Count} animators"); | ||||
| 
 | ||||
|             foreach (var discoBall in CachedDiscoBalls) | ||||
|             { | ||||
|                 discoBall.SetActive(true); | ||||
|             } | ||||
|             foreach (var animator in CachedDiscoBallAnimators) | ||||
|             { | ||||
|                 animator?.SetBool("on", on); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public static void Enable() | ||||
|         { | ||||
|             Toggle(true); | ||||
|         } | ||||
| 
 | ||||
|         public static void Disable() | ||||
|         { | ||||
|             Toggle(false); | ||||
|         } | ||||
| 
 | ||||
|         internal static void Clear() | ||||
|             foreach (var discoBall in InstantiatedContainers) | ||||
|             { | ||||
|             Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Clearing {CachedDiscoBalls.Count} disco balls & {CachedDiscoBallAnimators.Count} animators"); | ||||
|             CachedDiscoBallAnimators.Clear(); | ||||
|             CachedDiscoBalls.Clear(); | ||||
|                 Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)}: Disabling {discoBall.name}"); | ||||
|                 Destroy(discoBall); | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     [HarmonyPatch(typeof(Tile))] | ||||
|     static class DiscoBallTilePatch | ||||
|     { | ||||
|         [HarmonyPatch(nameof(Tile.AddTriggerVolume))] | ||||
|         [HarmonyPostfix] | ||||
|         static void OnAddTriggerVolume(Tile __instance) | ||||
|         { | ||||
|             DiscoBallManager.Patch(__instance); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [HarmonyPatch(typeof(RoundManager))] | ||||
|     static class DiscoBallDespawnPatch | ||||
|     { | ||||
|         [HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))] | ||||
|         [HarmonyPatch(nameof(RoundManager.OnDestroy))] | ||||
|         [HarmonyPrefix] | ||||
|         static void OnDestroy(RoundManager __instance) | ||||
|         { | ||||
|             var _ = __instance; | ||||
|             DiscoBallManager.Clear(); | ||||
|             InstantiatedContainers.Clear(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ namespace MuzikaGromche | |||
|                 .Select(a => $" Trying... {a}") | ||||
|         ]; | ||||
| 
 | ||||
|         public static readonly Track[] Tracks = [ | ||||
|         public static Track[] Tracks = [ | ||||
|             new Track | ||||
|             { | ||||
|                 Name = "MuzikaGromche", | ||||
|  | @ -482,8 +482,8 @@ namespace MuzikaGromche | |||
|             return tracks[trackId]; | ||||
|         } | ||||
| 
 | ||||
|         internal static Track? CurrentTrack; | ||||
|         internal static BeatTimeState? BeatTimeState; | ||||
|         public static Track? CurrentTrack; | ||||
|         public static BeatTimeState? BeatTimeState; | ||||
| 
 | ||||
|         public static void SetLightColor(Color color) | ||||
|         { | ||||
|  | @ -513,14 +513,7 @@ namespace MuzikaGromche | |||
|             return distance <= AudioMaxDistance; | ||||
|         } | ||||
| 
 | ||||
|         public static void DisplayLyrics(string text) | ||||
|         { | ||||
|             HUDManager.Instance.DisplayTip("[Lyrics]", text); | ||||
|             // Don't interrupt the music with constant HUD audio pings | ||||
|             HUDManager.Instance.UIAudio.Stop(); | ||||
|         } | ||||
| 
 | ||||
|         void Awake() | ||||
|         private void Awake() | ||||
|         { | ||||
|             string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); | ||||
|             UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2]; | ||||
|  | @ -544,16 +537,12 @@ namespace MuzikaGromche | |||
|                     track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]); | ||||
|                 } | ||||
|                 Config = new Config(base.Config); | ||||
|                 DiscoBallManager.Load(); | ||||
|                 DiscoBallManager.Initialize(); | ||||
|                 PoweredLightsAnimators.Load(); | ||||
|                 var harmony = new Harmony(PluginInfo.PLUGIN_NAME); | ||||
|                 harmony.PatchAll(typeof(JesterPatch)); | ||||
|                 harmony.PatchAll(typeof(EnemyAIPatch)); | ||||
|                 harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch)); | ||||
|                 harmony.PatchAll(typeof(AllPoweredLightsPatch)); | ||||
|                 harmony.PatchAll(typeof(DiscoBallTilePatch)); | ||||
|                 harmony.PatchAll(typeof(DiscoBallDespawnPatch)); | ||||
|                 harmony.PatchAll(typeof(SpawnRatePatch)); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|  | @ -563,7 +552,7 @@ namespace MuzikaGromche | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     public readonly record struct Language(string Short, string Full) | ||||
|     public record Language(string Short, string Full) | ||||
|     { | ||||
|         public static readonly Language ENGLISH = new("EN", "English"); | ||||
|         public static readonly Language RUSSIAN = new("RU", "Russian"); | ||||
|  | @ -588,9 +577,9 @@ namespace MuzikaGromche | |||
|                 ? Mathf.Pow(2f, 20f * x - 10f) / 2f | ||||
|                 : (2f - Mathf.Pow(2f, -20f * x + 10f)) / 2f); | ||||
| 
 | ||||
|         public static readonly Easing[] All = [Linear, InCubic, OutCubic, InOutCubic, InExpo, OutExpo, InOutExpo]; | ||||
|         public static Easing[] All = [Linear, InCubic, OutCubic, InOutCubic, InExpo, OutExpo, InOutExpo]; | ||||
| 
 | ||||
|         public static readonly string[] AllNames = [.. All.Select(easing => easing.Name)]; | ||||
|         public static string[] AllNames => [.. All.Select(easing => easing.Name)]; | ||||
| 
 | ||||
|         public static Easing FindByName(string Name) | ||||
|         { | ||||
|  | @ -603,9 +592,9 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public readonly record struct Palette(Color[] Colors) | ||||
|     public record Palette(Color[] Colors) | ||||
|     { | ||||
|         public static readonly Palette DEFAULT = new([Color.magenta, Color.cyan, Color.green, Color.yellow]); | ||||
|         public static Palette DEFAULT = new([Color.magenta, Color.cyan, Color.green, Color.yellow]); | ||||
| 
 | ||||
|         public static Palette Parse(string[] hexColors) | ||||
|         { | ||||
|  | @ -787,7 +776,7 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     readonly record struct BeatTimestamp | ||||
|     public readonly record struct BeatTimestamp | ||||
|     { | ||||
|         // Number of beats in the loop audio segment. | ||||
|         public readonly int LoopBeats; | ||||
|  | @ -837,7 +826,7 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     readonly record struct BeatTimeSpan | ||||
|     public readonly record struct BeatTimeSpan | ||||
|     { | ||||
|         public readonly int LoopBeats; | ||||
|         public readonly float HalfLoopBeats => LoopBeats / 2f; | ||||
|  | @ -987,7 +976,7 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class BeatTimeState | ||||
|     public class BeatTimeState | ||||
|     { | ||||
|         // The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet. | ||||
|         private bool hasStarted = false; | ||||
|  | @ -1254,9 +1243,9 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     abstract class BaseEvent; | ||||
|     public abstract class BaseEvent; | ||||
| 
 | ||||
|     class SetLightsColorEvent(Color color) : BaseEvent | ||||
|     public class SetLightsColorEvent(Color color) : BaseEvent | ||||
|     { | ||||
|         public readonly Color Color = color; | ||||
|         public override string ToString() | ||||
|  | @ -1265,7 +1254,7 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class SetLightsColorTransitionEvent(Color from, Color to, Easing easing, float t) | ||||
|     public class SetLightsColorTransitionEvent(Color from, Color to, Easing easing, float t) | ||||
|         : SetLightsColorEvent(Color.Lerp(from, to, Mathf.Clamp(easing.Eval(t), 0f, 1f))) | ||||
|     { | ||||
|         // Additional context for debugging | ||||
|  | @ -1279,12 +1268,12 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class FlickerLightsEvent : BaseEvent | ||||
|     public class FlickerLightsEvent : BaseEvent | ||||
|     { | ||||
|         public override string ToString() => "Flicker"; | ||||
|     } | ||||
| 
 | ||||
|     class LyricsEvent(string text) : BaseEvent | ||||
|     public class LyricsEvent(string text) : BaseEvent | ||||
|     { | ||||
|         public readonly string Text = text; | ||||
|         public override string ToString() | ||||
|  | @ -1293,14 +1282,14 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class WindUpZeroBeatEvent : BaseEvent | ||||
|     public class WindUpZeroBeatEvent : BaseEvent | ||||
|     { | ||||
|         public override string ToString() => "WindUp"; | ||||
|     } | ||||
| 
 | ||||
|     // Default C#/.NET remainder operator % returns negative result for negative input | ||||
|     // which is unsuitable as an index for an array. | ||||
|     static class Mod | ||||
|     public static class Mod | ||||
|     { | ||||
|         public static int Positive(int x, int m) | ||||
|         { | ||||
|  | @ -1320,7 +1309,7 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     readonly struct RandomWeightedIndex | ||||
|     public readonly struct RandomWeightedIndex | ||||
|     { | ||||
|         public RandomWeightedIndex(int[] weights) | ||||
|         { | ||||
|  | @ -1407,7 +1396,7 @@ namespace MuzikaGromche | |||
|         readonly public int TotalWeights { get; } | ||||
|     } | ||||
| 
 | ||||
|     static class SyncedEntryExtensions | ||||
|     public static class SyncedEntryExtensions | ||||
|     { | ||||
|         // Update local values on clients. Even though the clients couldn't | ||||
|         // edit them, they could at least see the new values. | ||||
|  | @ -1420,7 +1409,7 @@ namespace MuzikaGromche | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Config : SyncedConfig2<Config> | ||||
|     public class Config : SyncedConfig2<Config> | ||||
|     { | ||||
|         public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!; | ||||
| 
 | ||||
|  | @ -1428,8 +1417,6 @@ namespace MuzikaGromche | |||
| 
 | ||||
|         public static ConfigEntry<bool> SkipExplicitTracks { get; private set; } = null!; | ||||
| 
 | ||||
|         public static SyncedEntry<bool> OverrideSpawnRates { get; private set; } = null!; | ||||
| 
 | ||||
|         public static bool ShouldSkipWindingPhase { get; private set; } = false; | ||||
| 
 | ||||
|         public static Palette? PaletteOverride { get; private set; } = null; | ||||
|  | @ -1443,7 +1430,7 @@ namespace MuzikaGromche | |||
|         public static float? ColorTransitionOutOverride { get; private set; } = null; | ||||
|         public static string? ColorTransitionEasingOverride { get; private set; } = null; | ||||
| 
 | ||||
|         internal Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID) | ||||
|         public Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID) | ||||
|         { | ||||
|             DisplayLyrics = configFile.Bind("General", "Display Lyrics", true, | ||||
|                 new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music.")); | ||||
|  | @ -1458,11 +1445,6 @@ namespace MuzikaGromche | |||
|                 new ConfigDescription("When choosing tracks at random, skip the ones with Explicit Content/Lyrics.")); | ||||
|             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(SkipExplicitTracks, requiresRestart: false)); | ||||
| 
 | ||||
|             OverrideSpawnRates = configFile.BindSyncedEntry("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); | ||||
| 
 | ||||
| #if DEBUG | ||||
|             SetupEntriesToSkipWinding(configFile); | ||||
|             SetupEntriesForPaletteOverride(configFile); | ||||
|  | @ -1777,26 +1759,19 @@ namespace MuzikaGromche | |||
|     // farAudio is during windup, Start overrides popGoesTheWeaselTheme | ||||
|     // creatureVoice is when popped, Loop overrides screamingSFX | ||||
|     [HarmonyPatch(typeof(JesterAI))] | ||||
|     static class JesterPatch | ||||
|     internal class JesterPatch | ||||
|     { | ||||
| #if DEBUG | ||||
|         [HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))] | ||||
|         [HarmonyPostfix] | ||||
|         static void AlmostInstantFollowTimerPostfix(JesterAI __instance) | ||||
|         public static void AlmostInstantFollowTimerPostfix(JesterAI __instance) | ||||
|         { | ||||
|             __instance.beginCrankingTimer = 1f; | ||||
|         } | ||||
| #endif | ||||
| 
 | ||||
|         class State | ||||
|         { | ||||
|             public required AudioSource farAudio; | ||||
|             public required int previousState; | ||||
|         } | ||||
| 
 | ||||
|         [HarmonyPatch(nameof(JesterAI.Update))] | ||||
|         [HarmonyPrefix] | ||||
|         static void JesterUpdatePrefix(JesterAI __instance, out State __state) | ||||
|         public static void DoNotStopTheMusicPrefix(JesterAI __instance, out State __state) | ||||
|         { | ||||
|             __state = new State | ||||
|             { | ||||
|  | @ -1818,7 +1793,7 @@ namespace MuzikaGromche | |||
| 
 | ||||
|         [HarmonyPatch(nameof(JesterAI.Update))] | ||||
|         [HarmonyPostfix] | ||||
|         static void JesterUpdatePostfix(JesterAI __instance, State __state) | ||||
|         public static void DoNotStopTheMusic(JesterAI __instance, State __state) | ||||
|         { | ||||
|             if (__instance.previousState == 1 && __state.previousState != 1) | ||||
|             { | ||||
|  | @ -1900,7 +1875,9 @@ namespace MuzikaGromche | |||
|                         case LyricsEvent e: | ||||
|                             if (Plugin.LocalPlayerCanHearMusic(__instance)) | ||||
|                             { | ||||
|                                 Plugin.DisplayLyrics(e.Text); | ||||
|                                 HUDManager.Instance.DisplayTip("[Lyrics]", e.Text); | ||||
|                                 // Don't interrupt the music with constant HUD audio pings | ||||
|                                 HUDManager.Instance.UIAudio.Stop(); | ||||
|                             } | ||||
|                             break; | ||||
|                     } | ||||
|  | @ -1910,13 +1887,13 @@ namespace MuzikaGromche | |||
|     } | ||||
| 
 | ||||
|     [HarmonyPatch(typeof(EnemyAI))] | ||||
|     static class EnemyAIPatch | ||||
|     internal class EnemyAIPatch | ||||
|     { | ||||
|         // JesterAI class does not override abstract method OnDestroy, | ||||
|         // so we have to patch its superclass directly. | ||||
|         [HarmonyPatch(nameof(EnemyAI.OnDestroy))] | ||||
|         [HarmonyPrefix] | ||||
|         static void CleanUpOnDestroy(EnemyAI __instance) | ||||
|         public static void CleanUpOnDestroy(EnemyAI __instance) | ||||
|         { | ||||
|             if (__instance is JesterAI) | ||||
|             { | ||||
|  | @ -1928,4 +1905,10 @@ namespace MuzikaGromche | |||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     internal class State | ||||
|     { | ||||
|         public required AudioSource farAudio; | ||||
|         public required int previousState; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -9,17 +9,9 @@ using UnityEngine; | |||
| 
 | ||||
| namespace MuzikaGromche | ||||
| { | ||||
|     static class PoweredLightsAnimators | ||||
|     internal class PoweredLightsAnimators | ||||
|     { | ||||
|         private const string PoweredLightTag = "PoweredLight"; | ||||
| 
 | ||||
|         private delegate void ManualPatch(GameObject animatorContainer); | ||||
| 
 | ||||
|         private readonly record struct AnimatorPatch( | ||||
|             string AnimatorContainerPath, | ||||
|             RuntimeAnimatorController AnimatorController, | ||||
|             bool AddTagAndAnimator, | ||||
|             ManualPatch? ManualPatch); | ||||
|         private readonly record struct AnimatorPatch(string AnimatorContainerPath, RuntimeAnimatorController AnimatorController); | ||||
| 
 | ||||
|         private readonly record struct TilePatch(string TileName, AnimatorPatch[] Patches) | ||||
|         { | ||||
|  | @ -27,17 +19,13 @@ namespace MuzikaGromche | |||
|             public readonly string TileCloneName = $"{TileName}(Clone)"; | ||||
|         } | ||||
| 
 | ||||
|         private readonly record struct AnimatorPatchDescriptor( | ||||
|             string AnimatorContainerPath, | ||||
|             string AnimatorControllerAssetPath, | ||||
|             bool AddTagAndAnimator = false, | ||||
|             ManualPatch? ManualPatch = null) | ||||
|         private readonly record struct AnimatorPatchDescriptor(string AnimatorContainerPath, string AnimatorControllerAssetPath) | ||||
|         { | ||||
|             public AnimatorPatch Load(AssetBundle assetBundle) | ||||
|             { | ||||
|                 var animationController = assetBundle.LoadAsset<RuntimeAnimatorController>(AnimatorControllerAssetPath) | ||||
|                     ?? throw new FileNotFoundException($"RuntimeAnimatorController not found: {AnimatorControllerAssetPath}", AnimatorControllerAssetPath); | ||||
|                 return new(AnimatorContainerPath, animationController, AddTagAndAnimator, ManualPatch); | ||||
|                 return new(AnimatorContainerPath, animationController); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -57,10 +45,6 @@ namespace MuzikaGromche | |||
| 
 | ||||
|         private static IDictionary<string, TilePatch> Patches = new Dictionary<string, TilePatch>(); | ||||
| 
 | ||||
|         private static AudioClip AudioClipOn = null!; | ||||
|         private static AudioClip AudioClipOff = null!; | ||||
|         private static AudioClip AudioClipFlicker = null!; | ||||
| 
 | ||||
|         public static void Load() | ||||
|         { | ||||
|             const string BundleFileName = "muzikagromche_poweredlightsanimators"; | ||||
|  | @ -68,26 +52,16 @@ namespace MuzikaGromche | |||
|             var assetBundle = AssetBundle.LoadFromFile(bundlePath) | ||||
|                 ?? throw new NullReferenceException("Failed to load bundle"); | ||||
| 
 | ||||
|             AudioClipOn = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightOn.ogg"); | ||||
|             AudioClipOff = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightOff.ogg"); | ||||
|             AudioClipFlicker = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightFlicker.ogg"); | ||||
| 
 | ||||
|             const string BasePath = "Assets/LethalCompany/Mods/MuzikaGromche/AnimatorControllers/"; | ||||
| 
 | ||||
|             const string PointLight4 = $"{BasePath}Point Light (4) (Patched).controller"; | ||||
|             const string MineshaftSpotlight = $"{BasePath}MineshaftSpotlight (Patched).controller"; | ||||
|             const string LightbulbsLine = $"{BasePath}lightbulbsLineMesh (Patched).controller"; | ||||
|             const string CeilingFan = $"{BasePath}CeilingFan (originally GameObject) (Patched).controller"; | ||||
|             const string LEDHangingLight = $"{BasePath}LEDHangingLight (Patched).controller"; | ||||
|             const string MineshaftStartTileSpotlight = $"{BasePath}MineshaftStartTileSpotlight (New).controller"; | ||||
| 
 | ||||
| 
 | ||||
|             TilePatchDescriptor[] descriptors = | ||||
|             [ | ||||
|                 // any version | ||||
|                 new("KitchenTile", [ | ||||
|                     new("PoweredLightTypeB", PointLight4), | ||||
|                     new("PoweredLightTypeB (1)", PointLight4), | ||||
|                 ]), | ||||
|                 // < v70 | ||||
|                 new("ManorStartRoom", [ | ||||
|                     new("ManorStartRoom/Chandelier/PoweredLightTypeB (1)", PointLight4), | ||||
|  | @ -98,10 +72,6 @@ namespace MuzikaGromche | |||
|                     new("ManorStartRoomMesh/Chandelier/PoweredLightTypeB (1)", PointLight4), | ||||
|                     new("ManorStartRoomMesh/Chandelier2/PoweredLightTypeB", PointLight4), | ||||
|                 ]), | ||||
|                 new("NarrowHallwayTile2x2", [ | ||||
|                     new("MineshaftSpotlight (1)", MineshaftSpotlight), | ||||
|                     new("MineshaftSpotlight (2)", MineshaftSpotlight), | ||||
|                 ]), | ||||
|                 new("BirthdayRoomTile", [ | ||||
|                     new("Lights/MineshaftSpotlight", MineshaftSpotlight), | ||||
|                 ]), | ||||
|  | @ -113,21 +83,6 @@ namespace MuzikaGromche | |||
|                     new("CeilingFanAnimContainer", CeilingFan), | ||||
|                     new("MineshaftSpotlight (1)", MineshaftSpotlight), | ||||
|                 ]), | ||||
|                 new("GarageTile", [ | ||||
|                     new("HangingLEDBarLight (3)", LEDHangingLight), | ||||
|                     new("HangingLEDBarLight (4)", LEDHangingLight, | ||||
|                         // This HangingLEDBarLight's IndirectLight is wrongly named, so animator couldn't find it | ||||
|                         ManualPatch: RenameGameObjectPatch("IndirectLight (1)", "IndirectLight")), | ||||
|                 ]), | ||||
|                 new("PoolTile", [ | ||||
|                     new("PoolLights/HangingLEDBarLight", LEDHangingLight), | ||||
|                     new("PoolLights/HangingLEDBarLight (4)", LEDHangingLight), | ||||
|                     new("PoolLights/HangingLEDBarLight (5)", LEDHangingLight), | ||||
|                 ]), | ||||
|                 new("MineshaftStartTile", [ | ||||
|                     new("Cylinder.001 (1)", MineshaftStartTileSpotlight, AddTagAndAnimator: true), | ||||
|                     new("Cylinder.001 (2)", MineshaftStartTileSpotlight, AddTagAndAnimator: true), | ||||
|                 ]), | ||||
|             ]; | ||||
| 
 | ||||
|             Patches = descriptors | ||||
|  | @ -157,41 +112,7 @@ namespace MuzikaGromche | |||
| #pragma warning restore CS0162 // Unreachable code detected | ||||
|                     } | ||||
| 
 | ||||
|                     GameObject animationContainer = animationContainerTransform.gameObject; | ||||
|                     Animator animator = animationContainer.GetComponent<Animator>(); | ||||
| 
 | ||||
|                     if (patch.AddTagAndAnimator) | ||||
|                     { | ||||
|                         animationContainer.tag = PoweredLightTag; | ||||
|                         if (animator == null) | ||||
|                         { | ||||
|                             animator = animationContainer.AddComponent<Animator>(); | ||||
|                         } | ||||
|                         if (!animationContainer.TryGetComponent<PlayAudioAnimationEvent>(out var audioScript)) | ||||
|                         { | ||||
|                             audioScript = animationContainer.AddComponent<PlayAudioAnimationEvent>(); | ||||
|                             audioScript.audioClip = AudioClipOn; | ||||
|                             audioScript.audioClip2 = AudioClipOff; | ||||
|                             audioScript.audioClip3 = AudioClipFlicker; | ||||
|                         } | ||||
|                         if (!animationContainer.TryGetComponent<AudioSource>(out var audioSource)) | ||||
|                         { | ||||
|                             // Copy from an existing AudioSource of another light animator | ||||
|                             var otherSource = tile.gameObject.GetComponentInChildren<AudioSource>(); | ||||
|                             if (otherSource != null) | ||||
|                             { | ||||
|                                 audioSource = animationContainer.AddComponent<AudioSource>(); | ||||
|                                 audioSource.spatialBlend = 1; | ||||
|                                 audioSource.playOnAwake = false; | ||||
|                                 audioSource.outputAudioMixerGroup = otherSource.outputAudioMixerGroup; | ||||
|                                 audioSource.spread = otherSource.spread; | ||||
|                                 audioSource.rolloffMode = otherSource.rolloffMode; | ||||
|                                 audioSource.maxDistance = otherSource.maxDistance; | ||||
|                                 audioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, otherSource.GetCustomCurve(AudioSourceCurveType.CustomRolloff)); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     var animator = animationContainerTransform.gameObject.GetComponent<Animator>(); | ||||
|                     if (animator == null) | ||||
|                     { | ||||
| #if DEBUG | ||||
|  | @ -202,89 +123,21 @@ namespace MuzikaGromche | |||
| #pragma warning restore CS0162 // Unreachable code detected | ||||
|                     } | ||||
| 
 | ||||
|                     patch.ManualPatch?.Invoke(animationContainer); | ||||
| 
 | ||||
|                     animator.runtimeAnimatorController = patch.AnimatorController; | ||||
|                     Debug.Log($"{nameof(MuzikaGromche)} {nameof(PoweredLightsAnimatorsPatch)} {tilePatch.TileName}/{patch.AnimatorContainerPath}: Replaced animator controller"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private static ManualPatch RenameGameObjectPatch(string relativePath, string newName) => animatorContainer => | ||||
|         { | ||||
|             var targetObject = animatorContainer.transform.Find(relativePath)?.gameObject; | ||||
|             if (targetObject == null) | ||||
|             { | ||||
| #if DEBUG | ||||
|                 throw new NullReferenceException($"{animatorContainer.name}/{relativePath}: GameObject not found!"); | ||||
| #endif | ||||
| #pragma warning disable CS0162 // Unreachable code detected | ||||
|                 return; | ||||
| #pragma warning restore CS0162 // Unreachable code detected | ||||
|             } | ||||
|             targetObject.name = newName; | ||||
|             Debug.Log($"{nameof(MuzikaGromche)} {nameof(PoweredLightsAnimatorsPatch)} {animatorContainer.name}/{relativePath}: Renamed GameObject"); | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     [HarmonyPatch(typeof(Tile))] | ||||
|     static class PoweredLightsAnimatorsPatch | ||||
|     internal class PoweredLightsAnimatorsPatch | ||||
|     { | ||||
|         [HarmonyPatch(nameof(Tile.AddTriggerVolume))] | ||||
|         [HarmonyPatch("AddTriggerVolume")] | ||||
|         [HarmonyPostfix] | ||||
|         static void OnAddTriggerVolume(Tile __instance) | ||||
|         public static void OnAddTriggerVolume(Tile __instance) | ||||
|         { | ||||
|             PoweredLightsAnimators.Patch(__instance); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [HarmonyPatch(typeof(RoundManager))] | ||||
|     static class AllPoweredLightsPatch | ||||
|     { | ||||
|         // Vanilla method assumes that GameObjects with tag "PoweredLight" only contain a single Light component each. | ||||
|         // This is, however, not true for certains double-light setups, such as: | ||||
|         // - double PointLight (even though one of them is 'Point' another is 'Spot') inside CeilingFanAnimContainer in BedroomTile/BedroomTileB; | ||||
|         // - MineshaftSpotlight when it has not only `Point Light` but also `IndirectLight` in BirthdayRoomTile; | ||||
|         // - (maybe more?) | ||||
|         // In order to fix that, replace singular GetComponentInChildren<Light> with plural GetComponentsInChildren<Light> version. | ||||
|         [HarmonyPatch(nameof(RoundManager.RefreshLightsList))] | ||||
|         [HarmonyPrefix] | ||||
|         static bool OnRefreshLightsList(RoundManager __instance) | ||||
|         { | ||||
|             RefreshLightsListPatched(__instance); | ||||
|             // Skip the original method | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         static void RefreshLightsListPatched(RoundManager self) | ||||
|         { | ||||
|             // Reusable list to reduce allocations | ||||
|             List<Light> lights = []; | ||||
| 
 | ||||
|             self.allPoweredLights.Clear(); | ||||
|             self.allPoweredLightsAnimators.Clear(); | ||||
| 
 | ||||
|             GameObject[] gameObjects = GameObject.FindGameObjectsWithTag("PoweredLight"); | ||||
|             if (gameObjects == null) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             foreach (var gameObject in gameObjects) | ||||
|             { | ||||
|                 Animator animator = gameObject.GetComponentInChildren<Animator>(); | ||||
|                 if (!(animator == null)) | ||||
|                 { | ||||
|                     self.allPoweredLightsAnimators.Add(animator); | ||||
|                     // Patched section: Use list instead of singular GetComponentInChildren<Light> | ||||
|                     gameObject.GetComponentsInChildren(includeInactive: true, lights); | ||||
|                     self.allPoweredLights.AddRange(lights); | ||||
|                 } | ||||
|             } | ||||
|             foreach (var animator in self.allPoweredLightsAnimators) | ||||
|             { | ||||
|                 animator.SetFloat("flickerSpeed", UnityEngine.Random.Range(0.6f, 1.4f)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,69 +0,0 @@ | |||
| using HarmonyLib; | ||||
| using System; | ||||
| using UnityEngine; | ||||
| 
 | ||||
| namespace MuzikaGromche | ||||
| { | ||||
|     [HarmonyPatch(typeof(RoundManager))] | ||||
|     static class SpawnRatePatch | ||||
|     { | ||||
|         const string JesterEnemyName = "Jester"; | ||||
| 
 | ||||
|         // If set to null, do not override spawn chances. Otherwise, it is an index of Jester in RoundManager.currentLevel.Enemies | ||||
|         static int? JesterEnemyIndex = null; | ||||
|         static float? SpawnTime = null; | ||||
| 
 | ||||
|         [HarmonyPatch(nameof(RoundManager.AssignRandomEnemyToVent))] | ||||
|         [HarmonyPrefix] | ||||
|         static void AssignRandomEnemyToVentPrefix(RoundManager __instance, EnemyVent vent, float spawnTime) | ||||
|         { | ||||
|             if (!Config.OverrideSpawnRates.Value) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             var index = __instance.currentLevel.Enemies.FindIndex(enemy => enemy.enemyType.enemyName == JesterEnemyName); | ||||
|             if (index == -1) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             JesterEnemyIndex = index; | ||||
|             SpawnTime = spawnTime; | ||||
|         } | ||||
| 
 | ||||
|         [HarmonyPatch(nameof(RoundManager.AssignRandomEnemyToVent))] | ||||
|         [HarmonyPostfix] | ||||
|         static void AssignRandomEnemyToVentPostfix(RoundManager __instance, EnemyVent vent, float spawnTime) | ||||
|         { | ||||
|             JesterEnemyIndex = null; | ||||
|             SpawnTime = null; | ||||
|         } | ||||
| 
 | ||||
|         [HarmonyPatch(nameof(RoundManager.GetRandomWeightedIndex))] | ||||
|         [HarmonyPostfix] | ||||
|         static void GetRandomWeightedIndexPostfix(RoundManager __instance, ref int[] weights, ref System.Random randomSeed) | ||||
|         { | ||||
|             if (JesterEnemyIndex is int index && SpawnTime is float spawnTime) | ||||
|             { | ||||
|                 if (__instance.EnemyCannotBeSpawned(index)) | ||||
|                 { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 var minMultiplierTime = 3 * __instance.timeScript.lengthOfHours; | ||||
|                 var maxMultiplierTime = 7 * __instance.timeScript.lengthOfHours; | ||||
|                 var normalizedMultiplierTime = Math.Clamp((spawnTime - minMultiplierTime) / (maxMultiplierTime - minMultiplierTime), 0f, 1f); | ||||
| 
 | ||||
|                 // TODO: Try Expo function instead of Lerp? | ||||
|                 var minMultiplier = Mathf.Max(1, __instance.minEnemiesToSpawn); | ||||
|                 var maxMultiplier = 9 + __instance.minEnemiesToSpawn; | ||||
|                 var multiplier = Mathf.Lerp(minMultiplier, maxMultiplier, normalizedMultiplierTime); | ||||
| 
 | ||||
|                 var newWeight = (int)(weights[index] * multiplier); | ||||
|                 Debug.Log($"{nameof(MuzikaGromche)} Overriding Jester spawn weight {weights[index]} * {multiplier} => {newWeight}"); | ||||
|                 weights[index] = newWeight; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							
		Loading…
	
		Reference in New Issue