Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of VoiceLines v1.0.1
plugins/VoiceLines.dll
Decompiled 6 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("VoiceLines")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("VoiceLines")] [assembly: AssemblyTitle("VoiceLines")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace VoiceLines { internal sealed class MobVoiceController : MonoBehaviour { private static readonly Dictionary<string, AudioClip> ClipCache = new Dictionary<string, AudioClip>(StringComparer.OrdinalIgnoreCase); private Character _character; private MobVoiceSet _voiceSet; private AudioSource _source; private float _nextIdle; private bool _dead; internal static void Attach(Character character) { if (!((Object)(object)character == (Object)null) && !(character is Player) && !((Object)(object)((Component)character).GetComponent<MobVoiceController>() != (Object)null)) { string prefabName = ((Object)((Component)character).gameObject).name.Replace("(Clone)", string.Empty).Trim(); if (Plugin.Library.TryGet(prefabName, out MobVoiceSet voiceSet)) { ((Component)character).gameObject.AddComponent<MobVoiceController>().Setup(character, voiceSet); } } } private void Setup(Character character, MobVoiceSet voiceSet) { _character = character; _voiceSet = voiceSet; _source = ((Component)this).gameObject.AddComponent<AudioSource>(); _source.playOnAwake = false; _source.spatialBlend = Mathf.Clamp01(voiceSet.spatialBlend); _source.minDistance = Mathf.Max(0f, voiceSet.minDistance); _source.maxDistance = Mathf.Max(_source.minDistance, voiceSet.maxDistance); ScheduleIdle(); } private void Update() { if (!_dead && !(Time.time < _nextIdle) && !_source.isPlaying) { Play(_voiceSet.idle); ScheduleIdle(); } } internal void Hurt() { if (!_dead) { Play(_voiceSet.hurt); } } internal void Attack() { if (!_dead) { Play(_voiceSet.attack); } } internal void Die() { if (!_dead) { _dead = true; Play(_voiceSet.death); } } private void ScheduleIdle() { float num = Mathf.Max(1f, _voiceSet.idleMinSeconds); _nextIdle = Time.time + Random.Range(num, Mathf.Max(num, _voiceSet.idleMaxSeconds)); } private void Play(string[] clips) { string text = VoiceLineLibrary.Pick(clips); if (!string.IsNullOrWhiteSpace(text) && !_source.isPlaying && Plugin.TryResolveVoicePath(text, out string fullPath)) { if (ClipCache.TryGetValue(fullPath, out AudioClip value)) { PlayClip(value); } else { ((MonoBehaviour)this).StartCoroutine(LoadAndPlay(fullPath)); } } } private IEnumerator LoadAndPlay(string path) { UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(new Uri(path).AbsoluteUri, (AudioType)0); try { yield return request.SendWebRequest(); if ((int)request.result != 1) { Plugin.Log.LogWarning((object)("Unable to load voice clip '" + path + "': " + request.error)); yield break; } AudioClip content = DownloadHandlerAudioClip.GetContent(request); ClipCache[path] = content; PlayClip(content); } finally { ((IDisposable)request)?.Dispose(); } } private void PlayClip(AudioClip clip) { _source.volume = Mathf.Clamp01(_voiceSet.volume); _source.PlayOneShot(clip); } } internal static class Patches { internal static void Install(Harmony harmony) { PatchPostfix(harmony, typeof(Character), "Awake", "CharacterAwake"); PatchPostfix(harmony, typeof(Character), "RPC_Damage", "CharacterDamage"); PatchPostfix(harmony, typeof(Character), "RPC_OnDeath", "CharacterDeath"); PatchPostfix(harmony, typeof(Humanoid), "StartAttack", "HumanoidAttack"); } private static void PatchPostfix(Harmony harmony, Type targetType, string methodName, string patchMethodName) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(targetType, methodName, (Type[])null, (Type[])null); if (methodInfo == null) { Plugin.Log.LogWarning((object)("Voice Lines could not find " + targetType.Name + "." + methodName + "; that voice event is disabled.")); } else { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(Patches), patchMethodName, (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } private static void CharacterAwake(Character __instance) { MobVoiceController.Attach(__instance); } private static void CharacterDamage(Character __instance) { ((Component)__instance).GetComponent<MobVoiceController>()?.Hurt(); } private static void CharacterDeath(Character __instance) { ((Component)__instance).GetComponent<MobVoiceController>()?.Die(); } private static void HumanoidAttack(Humanoid __instance) { ((Component)__instance).GetComponent<MobVoiceController>()?.Attack(); } } [BepInPlugin("com.claytonwebb.valheim.voicelines", "Voice Lines", "1.0.1")] public sealed class Plugin : BaseUnityPlugin { internal const string PluginGuid = "com.claytonwebb.valheim.voicelines"; internal const string PluginName = "Voice Lines"; internal const string PluginVersion = "1.0.1"; internal static Plugin Instance { get; private set; } = null; internal static string VoiceDirectory { get; private set; } = string.Empty; internal static string BundledVoiceDirectory { get; private set; } = string.Empty; internal static VoiceLineLibrary Library { get; private set; } = null; internal static ManualLogSource Log => ((BaseUnityPlugin)Instance).Logger; private void Awake() { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Expected O, but got Unknown Instance = this; VoiceDirectory = Path.Combine(Paths.ConfigPath, "VoiceLines"); BundledVoiceDirectory = Path.Combine(Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location), "VoiceLinesWAV"); Directory.CreateDirectory(VoiceDirectory); Library = new VoiceLineLibrary(Path.Combine(VoiceDirectory, "voices.json"), ((BaseUnityPlugin)this).Logger); Library.LoadOrCreateExample(); Patches.Install(new Harmony("com.claytonwebb.valheim.voicelines")); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Voice Lines loaded. Add audio and edit " + VoiceDirectory)); } internal static bool TryResolveVoicePath(string relativePath, out string fullPath) { string[] array = new string[2] { VoiceDirectory, BundledVoiceDirectory }; foreach (string obj in array) { string fullPath2 = Path.GetFullPath(obj); char directorySeparatorChar = Path.DirectorySeparatorChar; string value = fullPath2 + directorySeparatorChar; string fullPath3 = Path.GetFullPath(Path.Combine(obj, relativePath)); if (fullPath3.StartsWith(value, StringComparison.OrdinalIgnoreCase) && File.Exists(fullPath3)) { fullPath = fullPath3; return true; } } fullPath = string.Empty; return false; } } internal sealed class VoiceLineLibrary { private readonly string _configPath; private readonly ManualLogSource _logger; private Dictionary<string, MobVoiceSet> _mobs = new Dictionary<string, MobVoiceSet>(StringComparer.OrdinalIgnoreCase); private const string ExampleJson = "{\n \"mobs\": [\n {\n \"prefab\": \"Greydwarf\",\n \"volume\": 0.85,\n \"spatialBlend\": 1.0,\n \"minDistance\": 4.0,\n \"maxDistance\": 30.0,\n \"idleMinSeconds\": 12.0,\n \"idleMaxSeconds\": 28.0,\n \"idle\": [\"greydwarf/idle_01.ogg\"],\n \"hurt\": [\"greydwarf/hurt_01.ogg\"],\n \"death\": [\"greydwarf/death_01.ogg\"],\n \"attack\": [\"greydwarf/attack_01.ogg\"]\n },\n {\n \"prefab\": \"Greydwarf_Elite\",\n \"volume\": 1.0,\n \"spatialBlend\": 1.0,\n \"minDistance\": 6.0,\n \"maxDistance\": 45.0,\n \"idleMinSeconds\": 18.0,\n \"idleMaxSeconds\": 38.0,\n \"idle\": [\"VoiceLinesWAV/Greydwarf Brute - idle chatter.wav\"],\n \"hurt\": [\"VoiceLinesWAV/Greydwarf Brute - taking damage.wav\"],\n \"death\": [\"VoiceLinesWAV/Greydwarf Brute - dying.wav\"],\n \"attack\": [\"VoiceLinesWAV/Greydwarf Brute - beginning an attack.wav\"]\n }\n ]\n}"; internal VoiceLineLibrary(string configPath, ManualLogSource logger) { _configPath = configPath; _logger = logger; } internal void LoadOrCreateExample() { if (!File.Exists(_configPath)) { File.WriteAllText(_configPath, "{\n \"mobs\": [\n {\n \"prefab\": \"Greydwarf\",\n \"volume\": 0.85,\n \"spatialBlend\": 1.0,\n \"minDistance\": 4.0,\n \"maxDistance\": 30.0,\n \"idleMinSeconds\": 12.0,\n \"idleMaxSeconds\": 28.0,\n \"idle\": [\"greydwarf/idle_01.ogg\"],\n \"hurt\": [\"greydwarf/hurt_01.ogg\"],\n \"death\": [\"greydwarf/death_01.ogg\"],\n \"attack\": [\"greydwarf/attack_01.ogg\"]\n },\n {\n \"prefab\": \"Greydwarf_Elite\",\n \"volume\": 1.0,\n \"spatialBlend\": 1.0,\n \"minDistance\": 6.0,\n \"maxDistance\": 45.0,\n \"idleMinSeconds\": 18.0,\n \"idleMaxSeconds\": 38.0,\n \"idle\": [\"VoiceLinesWAV/Greydwarf Brute - idle chatter.wav\"],\n \"hurt\": [\"VoiceLinesWAV/Greydwarf Brute - taking damage.wav\"],\n \"death\": [\"VoiceLinesWAV/Greydwarf Brute - dying.wav\"],\n \"attack\": [\"VoiceLinesWAV/Greydwarf Brute - beginning an attack.wav\"]\n }\n ]\n}"); } Reload(); } internal void Reload() { try { _mobs = (JsonUtility.FromJson<VoiceConfig>(File.ReadAllText(_configPath))?.mobs ?? Array.Empty<MobVoiceSet>()).Where((MobVoiceSet m) => !string.IsNullOrWhiteSpace(m.prefab)).ToDictionary<MobVoiceSet, string>((MobVoiceSet m) => m.prefab, StringComparer.OrdinalIgnoreCase); _logger.LogInfo((object)$"Loaded {_mobs.Count} mob voice set(s)."); } catch (Exception ex) { _logger.LogError((object)("Could not read voices.json: " + ex.Message)); } } internal bool TryGet(string prefabName, out MobVoiceSet voiceSet) { return _mobs.TryGetValue(prefabName, out voiceSet); } internal static string Pick(string[]? clips) { if (clips != null && clips.Length != 0) { return clips[Random.Range(0, clips.Length)]; } return string.Empty; } } [Serializable] internal sealed class VoiceConfig { public MobVoiceSet[] mobs = Array.Empty<MobVoiceSet>(); } [Serializable] internal sealed class MobVoiceSet { public string prefab = string.Empty; public float volume = 1f; public float spatialBlend = 1f; public float minDistance = 4f; public float maxDistance = 30f; public float idleMinSeconds = 15f; public float idleMaxSeconds = 30f; public string[] idle = Array.Empty<string>(); public string[] hurt = Array.Empty<string>(); public string[] death = Array.Empty<string>(); public string[] attack = Array.Empty<string>(); } }