Decompiled source of VoiceLines v1.0.1

plugins/VoiceLines.dll

Decompiled 6 hours ago
using 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>();
	}
}