diff --git a/MuzikaGromche/Plugin.cs b/MuzikaGromche/Plugin.cs index 012da97..67baa47 100644 --- a/MuzikaGromche/Plugin.cs +++ b/MuzikaGromche/Plugin.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using BepInEx; +using BepInEx.Configuration; using HarmonyLib; using UnityEngine; using UnityEngine.Networking; @@ -60,23 +61,15 @@ namespace MuzikaGromche public static Track ChooseTrack() { var seed = RoundManager.Instance.dungeonGenerator.Generator.ChosenSeed; - var sha = SHA256.Create(); - var hash = sha.ComputeHash(BitConverter.GetBytes(seed)); - var trackId = 0; - foreach (var t in hash) - { - // modulus division on byte array - trackId *= 256 % Tracks.Length; - trackId %= Tracks.Length; - trackId += t % Tracks.Length; - trackId %= Tracks.Length; - } + int[] weights = [.. Tracks.Select(track => track.Weight.Value)]; + var rwi = new RandomWeightedIndex(weights); + var trackId = rwi.GetRandomWeightedIndex(seed); #if DEBUG // Override for testing trackId = IndexOfTrack("DeployDestroy"); #endif var track = Tracks[trackId]; - Debug.Log($"Seed is {seed}, chosen track is \"{track.Name}\", {trackId} out of {Tracks.Length} tracks"); + Debug.Log($"Seed is {seed}, chosen track is \"{track.Name}\", #{trackId} of {rwi}"); return Tracks[trackId]; } @@ -158,6 +151,13 @@ namespace MuzikaGromche track.LoadedStart = DownloadHandlerAudioClip.GetContent(requests[i * 2]); track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]); } + // Initialize config + var chanceRange = new AcceptableValueRange(0, 100); + foreach (var track in Tracks) + { + string description = $"Random (relative) chance of selecting track {track.Name}. Set to zero to effectively disable the track."; + track.Weight = Config.Bind("Tracks", track.Name, 50, new ConfigDescription(description, chanceRange)); + } new Harmony(PluginInfo.PLUGIN_NAME).PatchAll(typeof(JesterPatch)); } else @@ -186,6 +186,9 @@ namespace MuzikaGromche public AudioClip LoadedStart; public AudioClip LoadedLoop; + // How often this track should be chosen, relative to the sum of weights of all tracks. + public ConfigEntry Weight; + public string FileNameStart => $"{Name}Start.{Ext}"; public string FileNameLoop => $"{Name}Loop.{Ext}"; private string Ext => AudioType switch @@ -197,6 +200,93 @@ namespace MuzikaGromche }; } + public readonly struct RandomWeightedIndex + { + public RandomWeightedIndex(int[] weights) + { + Weights = weights; + TotalWeights = Weights.Sum(); + if (TotalWeights == 0) + { + // If everything is set to zero, everything is equally possible + Weights = [.. Weights.Select(_ => 1)]; + TotalWeights = Weights.Length; + } + } + + private byte[] GetHash(int seed) + { + var buffer = new byte[4 * (1 + Weights.Length)]; + var offset = 0; + System.Buffer.BlockCopy(BitConverter.GetBytes(seed), 0, buffer, offset, sizeof(int)); + // Make sure that tweaking weights even a little drastically changes the outcome + foreach (var weight in Weights) + { + offset += 4; + System.Buffer.BlockCopy(BitConverter.GetBytes(weight), 0, buffer, offset, sizeof(int)); + } + var sha = SHA256.Create(); + var hash = sha.ComputeHash(buffer); + return hash; + } + + private int GetRawIndex(byte[] hash) + { + if (TotalWeights == 0) + { + // Should not happen, but what if Weights array is empty? + return -1; + } + + var index = 0; + foreach (var t in hash) + { + // modulus division on byte array + index *= 256 % TotalWeights; + index %= TotalWeights; + index += t % TotalWeights; + index %= TotalWeights; + } + return index; + } + + private int GetRawIndex(int rawIndex) + { + if (rawIndex < 0 || rawIndex >= TotalWeights) + { + return -1; + } + + int sum = 0; + foreach (var (weight, index) in Weights.Select((x, i) => (x, i))) + { + sum += weight; + if (rawIndex < sum) + { + // Found + return index; + } + } + + return -1; + } + + public int GetRandomWeightedIndex(int seed) + { + var hash = GetHash(seed); + var index = GetRawIndex(hash); + return GetRawIndex(index); + } + + public override string ToString() + { + return $"Weighted(Total={TotalWeights}, Weights=[{string.Join(',', Weights)}])"; + } + + readonly private int[] Weights; + readonly public int TotalWeights { get; } + } + [HarmonyPatch(typeof(JesterAI))] internal class JesterPatch {