Decompiled source of Valheim Music Plus v1.14.3

plugins/BiomeMusic/BiomeMusic.dll

Decompiled 2 weeks ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using UnityEngine;
using UnityEngine.Audio;
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("BiomeMusic")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("BiomeMusic")]
[assembly: AssemblyTitle("BiomeMusic")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace BiomeMusic;

[BepInPlugin("kadabra.biomemusic", "BiomeMusic", "1.14.3")]
public class BiomeMusicPlugin : BaseUnityPlugin
{
	private static ManualLogSource Log;

	private ConfigEntry<float> cfgVolume;

	private ConfigEntry<float> cfgMinGap;

	private ConfigEntry<float> cfgMaxGap;

	private ConfigEntry<float> cfgFadeTime;

	private ConfigEntry<float> cfgBossRadius;

	private ConfigEntry<float> cfgBuildingRadius;

	private ConfigEntry<float> cfgHomeGrace;

	private ConfigEntry<float> cfgFireplaceRadius;

	private ConfigEntry<bool> cfgMiningMusic;

	private ConfigEntry<float> cfgMiningLinger;

	private AudioSource source;

	private readonly Dictionary<string, List<AudioClip>> clips = new Dictionary<string, List<AudioClip>>();

	private readonly List<AudioClip> globalClips = new List<AudioClip>();

	private readonly Dictionary<string, (AudioClip clip, float pos)> _saved = new Dictionary<string, (AudioClip, float)>();

	private readonly Dictionary<string, Queue<AudioClip>> _queues = new Dictionary<string, Queue<AudioClip>>();

	private string _currentZone = "";

	private bool _isFading;

	private bool _skipNextGap;

	private float _lastShelterTime = -999f;

	private float _lastMineTime = -999f;

	private float _fade = 1f;

	private static readonly Dictionary<string, string> BossFolders = new Dictionary<string, string>
	{
		{ "eikthyr", "eikthyr" },
		{ "elder", "gd_king" },
		{ "bonemass", "bonemass" },
		{ "moder", "dragon" },
		{ "yagluth", "goblinking" },
		{ "queen", "seekerqueen" },
		{ "fader", "fader" }
	};

	private void Awake()
	{
		Log = ((BaseUnityPlugin)this).Logger;
		cfgVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "Volume", 0.8f, "Volume of your tracks (0.0-1.0)");
		cfgMinGap = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "MinGap", 1f, "Min seconds gap between tracks (natural endings)");
		cfgMaxGap = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "MaxGap", 3f, "Max seconds gap between tracks (natural endings)");
		cfgFadeTime = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "FadeTime", 2f, "Seconds to fade in/out on zone change");
		cfgBossRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Detection", "BossRadius", 150f, "Meters to detect a nearby boss");
		cfgBuildingRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Detection", "BuildingRadius", 1f, "Meters to detect nearby structural pieces");
		cfgHomeGrace = ((BaseUnityPlugin)this).Config.Bind<float>("Detection", "HomeGrace", 3f, "Seconds home music lingers after leaving shelter");
		cfgFireplaceRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Detection", "FireplaceRadius", 12f, "Meters to detect a burning fireplace outdoors");
		cfgMiningMusic = ((BaseUnityPlugin)this).Config.Bind<bool>("Mining", "Enabled", true, "EXPERIMENTAL: play mining/ tracks while swinging a pickaxe");
		cfgMiningLinger = ((BaseUnityPlugin)this).Config.Bind<float>("Mining", "Linger", 20f, "Seconds mining music keeps playing after the last pickaxe swing");
		string text = Path.Combine(Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location), "music");
		if (!Directory.Exists(text))
		{
			text = Path.Combine(Paths.PluginPath, "BiomeMusic", "music");
		}
		Log.LogInfo((object)("[BiomeMusic] Music folder: " + text));
		string text2 = Path.Combine(Paths.ConfigPath, "BiomeMusic", "music-personal");
		string text3 = Path.Combine(Path.GetDirectoryName(text), "music-personal");
		Log.LogInfo((object)("[BiomeMusic] Personal folder: " + text2));
		((MonoBehaviour)this).StartCoroutine(LoadAllClips(text, text2, text3));
	}

	private void SilenceMusicMan()
	{
		MusicMan instance = MusicMan.instance;
		if ((Object)(object)instance == (Object)null)
		{
			return;
		}
		AudioSource[] componentsInChildren = ((Component)instance).GetComponentsInChildren<AudioSource>();
		foreach (AudioSource val in componentsInChildren)
		{
			if ((Object)(object)val != (Object)null && val.isPlaying)
			{
				val.Stop();
			}
		}
	}

	private void AttachToMusicMixer()
	{
		if ((Object)(object)source == (Object)null || (Object)(object)source.outputAudioMixerGroup != (Object)null)
		{
			return;
		}
		AudioMan instance = AudioMan.instance;
		if ((Object)(object)instance == (Object)null)
		{
			return;
		}
		AudioMixerGroup val = null;
		try
		{
			object? obj = typeof(AudioMan).GetField("m_musicMixer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance);
			val = (AudioMixerGroup)((obj is AudioMixerGroup) ? obj : null);
		}
		catch
		{
		}
		if ((Object)(object)val == (Object)null)
		{
			List<AudioSource> list = new List<AudioSource>(((Component)instance).GetComponentsInChildren<AudioSource>(true));
			if ((Object)(object)MusicMan.instance != (Object)null)
			{
				list.AddRange(((Component)MusicMan.instance).GetComponentsInChildren<AudioSource>(true));
			}
			foreach (AudioSource item in list)
			{
				AudioMixer val2 = (((Object)(object)item != (Object)null && (Object)(object)item.outputAudioMixerGroup != (Object)null) ? item.outputAudioMixerGroup.audioMixer : null);
				if ((Object)(object)val2 == (Object)null)
				{
					continue;
				}
				AudioMixerGroup[] array = val2.FindMatchingGroups("Music");
				foreach (AudioMixerGroup val3 in array)
				{
					if (((Object)val3).name == "Music")
					{
						val = val3;
						break;
					}
				}
				if ((Object)(object)val != (Object)null)
				{
					break;
				}
			}
		}
		if (!((Object)(object)val == (Object)null))
		{
			source.outputAudioMixerGroup = val;
			Log.LogInfo((object)("[BiomeMusic] Using the game's music channel (" + ((Object)val).name + ") (same as vanilla music)"));
		}
	}

	private IEnumerator VanillaSilencer()
	{
		while (true)
		{
			AttachToMusicMixer();
			if ((Object)(object)Player.m_localPlayer != (Object)null)
			{
				SilenceMusicMan();
			}
			yield return (object)new WaitForSeconds(0.5f);
		}
	}

	private void Update()
	{
		//IL_0068: Unknown result type (might be due to invalid IL or missing references)
		//IL_006f: Invalid comparison between Unknown and I4
		if ((Object)(object)source != (Object)null)
		{
			source.volume = _fade * cfgVolume.Value * MusicMan.m_masterMusicVolume;
		}
		if (cfgMiningMusic == null || !cfgMiningMusic.Value)
		{
			return;
		}
		Player localPlayer = Player.m_localPlayer;
		if ((Object)(object)localPlayer == (Object)null)
		{
			return;
		}
		try
		{
			ItemData currentWeapon = ((Humanoid)localPlayer).GetCurrentWeapon();
			if (currentWeapon != null && (int)currentWeapon.m_shared.m_skillType == 12 && ((Character)localPlayer).InAttack())
			{
				_lastMineTime = Time.time;
			}
		}
		catch
		{
		}
	}

	private bool IsMining()
	{
		if (cfgMiningMusic.Value)
		{
			return Time.time - _lastMineTime < cfgMiningLinger.Value;
		}
		return false;
	}

	private bool HasClips(string zone)
	{
		if (clips.TryGetValue(zone, out var value))
		{
			return value.Count > 0;
		}
		return false;
	}

	private AudioClip NextClip(string zone)
	{
		if (!clips.ContainsKey(zone) || clips[zone].Count == 0)
		{
			return null;
		}
		if (!_queues.ContainsKey(zone) || _queues[zone].Count == 0)
		{
			List<AudioClip> list = new List<AudioClip>(clips[zone]);
			for (int num = list.Count - 1; num > 0; num--)
			{
				int index = Random.Range(0, num + 1);
				AudioClip value = list[num];
				list[num] = list[index];
				list[index] = value;
			}
			_queues[zone] = new Queue<AudioClip>(list);
			Log.LogInfo((object)$"[BiomeMusic] [{zone}] reshuffled ({list.Count} tracks)");
		}
		return _queues[zone].Dequeue();
	}

	private IEnumerator FadeOutAndStop()
	{
		_isFading = true;
		float startVol = _fade;
		float elapsed = 0f;
		float duration = cfgFadeTime.Value;
		while (elapsed < duration && source.isPlaying)
		{
			_fade = Mathf.Lerp(startVol, 0f, elapsed / duration);
			elapsed += Time.deltaTime;
			yield return null;
		}
		source.Stop();
		_fade = 1f;
		_isFading = false;
	}

	private IEnumerator FadeIn()
	{
		float elapsed = 0f;
		float duration = cfgFadeTime.Value;
		while (elapsed < duration)
		{
			_fade = Mathf.Lerp(0f, 1f, elapsed / duration);
			elapsed += Time.deltaTime;
			yield return null;
		}
		_fade = 1f;
	}

	private Character GetNearbyBoss(Player player)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			Collider[] array = Physics.OverlapSphere(((Component)player).transform.position, cfgBossRadius.Value);
			for (int i = 0; i < array.Length; i++)
			{
				Character componentInParent = ((Component)array[i]).GetComponentInParent<Character>();
				if ((Object)(object)componentInParent != (Object)null && (Object)(object)componentInParent != (Object)(object)player && componentInParent.IsBoss() && !componentInParent.IsDead())
				{
					return componentInParent;
				}
			}
		}
		catch
		{
		}
		return null;
	}

	private static string BossZone(string prefabOrFolder)
	{
		string text = prefabOrFolder.ToLowerInvariant();
		if (BossFolders.TryGetValue(text, out var value))
		{
			text = value;
		}
		return "boss_" + text;
	}

	private string GetBossKey(Player player)
	{
		Character nearbyBoss = GetNearbyBoss(player);
		if ((Object)(object)nearbyBoss == (Object)null)
		{
			return null;
		}
		string text = BossZone(((Object)((Component)nearbyBoss).gameObject).name.Replace("(Clone)", "").Trim());
		if (HasClips(text))
		{
			return text;
		}
		if (HasClips("boss"))
		{
			return "boss";
		}
		return null;
	}

	private bool IsNearFireplace(Player player)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			Collider[] array = Physics.OverlapSphere(((Component)player).transform.position, cfgFireplaceRadius.Value);
			for (int i = 0; i < array.Length; i++)
			{
				Fireplace componentInParent = ((Component)array[i]).GetComponentInParent<Fireplace>();
				if ((Object)(object)componentInParent != (Object)null && componentInParent.IsBurning())
				{
					return true;
				}
			}
		}
		catch
		{
		}
		return false;
	}

	private bool IsNearBuilding(Player player)
	{
		//IL_0008: Unknown result type (might be due to invalid IL or missing references)
		//IL_003d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0042: Unknown result type (might be due to invalid IL or missing references)
		//IL_0046: Unknown result type (might be due to invalid IL or missing references)
		//IL_0054: Unknown result type (might be due to invalid IL or missing references)
		//IL_0059: Unknown result type (might be due to invalid IL or missing references)
		//IL_005d: Unknown result type (might be due to invalid IL or missing references)
		//IL_006b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0070: Unknown result type (might be due to invalid IL or missing references)
		//IL_0074: Unknown result type (might be due to invalid IL or missing references)
		int num = 0;
		Collider[] array = Physics.OverlapSphere(((Component)player).transform.position, cfgBuildingRadius.Value);
		foreach (Collider val in array)
		{
			if ((Object)(object)((Component)val).GetComponentInParent<WearNTear>() != (Object)null)
			{
				float[] array2 = new float[3];
				Bounds bounds = val.bounds;
				array2[0] = ((Bounds)(ref bounds)).size.x;
				bounds = val.bounds;
				array2[1] = ((Bounds)(ref bounds)).size.y;
				bounds = val.bounds;
				array2[2] = ((Bounds)(ref bounds)).size.z;
				if (Mathf.Max(array2) > 0.8f && ++num >= 5)
				{
					return true;
				}
			}
		}
		return false;
	}

	private string GetBiomeKey(Player player)
	{
		//IL_0087: Unknown result type (might be due to invalid IL or missing references)
		//IL_008c: Unknown result type (might be due to invalid IL or missing references)
		//IL_008d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0090: Invalid comparison between Unknown and I4
		//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b8: Invalid comparison between Unknown and I4
		//IL_0092: Unknown result type (might be due to invalid IL or missing references)
		//IL_0094: Unknown result type (might be due to invalid IL or missing references)
		//IL_00aa: Expected I4, but got Unknown
		//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cc: Invalid comparison between Unknown and I4
		//IL_00ba: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bd: Invalid comparison between Unknown and I4
		//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ac: Invalid comparison between Unknown and I4
		//IL_00ce: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d4: Invalid comparison between Unknown and I4
		//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c2: Invalid comparison between Unknown and I4
		//IL_00ae: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b1: Invalid comparison between Unknown and I4
		string bossKey = GetBossKey(player);
		if (bossKey != null)
		{
			return bossKey;
		}
		if (HasClips("mining") && IsMining())
		{
			return "mining";
		}
		bool num = player.InShelter();
		bool flag = IsNearFireplace(player);
		if (num && flag)
		{
			_lastShelterTime = Time.time;
			return "home";
		}
		if (Time.time - _lastShelterTime < cfgHomeGrace.Value)
		{
			return "home";
		}
		if (HasClips("approaching") && IsNearBuilding(player))
		{
			return "approaching";
		}
		Biome currentBiome = player.GetCurrentBiome();
		if ((int)currentBiome <= 16)
		{
			switch (currentBiome - 1)
			{
			default:
				if ((int)currentBiome != 8)
				{
					if ((int)currentBiome != 16)
					{
						break;
					}
					return "plains";
				}
				return "blackforest";
			case 0:
				return "meadows";
			case 1:
				return "swamp";
			case 3:
				return "mountain";
			case 2:
				break;
			}
		}
		else if ((int)currentBiome <= 64)
		{
			if ((int)currentBiome == 32)
			{
				return "ashlands";
			}
			if ((int)currentBiome == 64)
			{
				return "deepnorth";
			}
		}
		else
		{
			if ((int)currentBiome == 256)
			{
				return "ocean";
			}
			if ((int)currentBiome == 512)
			{
				return "mistlands";
			}
		}
		return "meadows";
	}

	private IEnumerator LoadAllClips(params string[] roots)
	{
		string[] biomes = new string[13]
		{
			"meadows", "blackforest", "swamp", "mountain", "plains", "ocean", "mistlands", "ashlands", "deepnorth", "home",
			"boss", "approaching", "mining"
		};
		foreach (string root in roots)
		{
			if (!Directory.Exists(root))
			{
				continue;
			}
			string[] array = biomes;
			foreach (string biome in array)
			{
				string dir = Path.Combine(root, biome);
				if (Directory.Exists(dir))
				{
					if (!clips.ContainsKey(biome))
					{
						clips[biome] = new List<AudioClip>();
					}
					string[] files = Directory.GetFiles(dir, "*.ogg");
					foreach (string path in files)
					{
						yield return ((MonoBehaviour)this).StartCoroutine(LoadClip(path, clips[biome], (AudioType)14));
					}
					files = Directory.GetFiles(dir, "*.wav");
					foreach (string path2 in files)
					{
						yield return ((MonoBehaviour)this).StartCoroutine(LoadClip(path2, clips[biome], (AudioType)20));
					}
				}
			}
			string path3 = Path.Combine(root, "boss");
			if (Directory.Exists(path3))
			{
				array = Directory.GetDirectories(path3);
				foreach (string biome in array)
				{
					string dir = BossZone(Path.GetFileName(biome));
					if (!clips.ContainsKey(dir))
					{
						clips[dir] = new List<AudioClip>();
					}
					string[] files = Directory.GetFiles(biome, "*.ogg");
					foreach (string path4 in files)
					{
						yield return ((MonoBehaviour)this).StartCoroutine(LoadClip(path4, clips[dir], (AudioType)14));
					}
					files = Directory.GetFiles(biome, "*.wav");
					foreach (string path5 in files)
					{
						yield return ((MonoBehaviour)this).StartCoroutine(LoadClip(path5, clips[dir], (AudioType)20));
					}
				}
			}
			string globalDir = Path.Combine(root, "global");
			if (Directory.Exists(globalDir))
			{
				array = Directory.GetFiles(globalDir, "*.ogg");
				foreach (string path6 in array)
				{
					yield return ((MonoBehaviour)this).StartCoroutine(LoadClip(path6, globalClips, (AudioType)14));
				}
				array = Directory.GetFiles(globalDir, "*.wav");
				foreach (string path7 in array)
				{
					yield return ((MonoBehaviour)this).StartCoroutine(LoadClip(path7, globalClips, (AudioType)20));
				}
			}
		}
		foreach (KeyValuePair<string, List<AudioClip>> clip in clips)
		{
			Log.LogInfo((object)$"[BiomeMusic] {clip.Key}: {clip.Value.Count} track(s)");
		}
		if (globalClips.Count > 0)
		{
			Log.LogInfo((object)$"[BiomeMusic] global: {globalClips.Count} track(s)");
			string[] array2 = new string[11]
			{
				"meadows", "blackforest", "swamp", "mountain", "plains", "ocean", "mistlands", "ashlands", "deepnorth", "home",
				"approaching"
			};
			foreach (string key in array2)
			{
				if (!clips.ContainsKey(key))
				{
					clips[key] = new List<AudioClip>();
				}
				clips[key].AddRange(globalClips);
			}
		}
		GameObject val = new GameObject("BiomeMusicPlayer");
		Object.DontDestroyOnLoad((Object)(object)val);
		source = val.AddComponent<AudioSource>();
		source.spatialBlend = 0f;
		source.loop = false;
		_fade = 1f;
		int num = 0;
		foreach (List<AudioClip> value in clips.Values)
		{
			num += value.Count;
		}
		Log.LogInfo((object)$"[BiomeMusic] Ready — {num} total slots.");
		if (num == 0)
		{
			Log.LogWarning((object)("[BiomeMusic] No tracks found in " + string.Join(", ", roots) + " — leaving vanilla music on."));
			yield break;
		}
		((MonoBehaviour)this).StartCoroutine(VanillaSilencer());
		((MonoBehaviour)this).StartCoroutine(MusicLoop());
	}

	private IEnumerator LoadClip(string path, List<AudioClip> target, AudioType type)
	{
		//IL_0015: Unknown result type (might be due to invalid IL or missing references)
		//IL_0016: Unknown result type (might be due to invalid IL or missing references)
		UnityWebRequest req = UnityWebRequestMultimedia.GetAudioClip("file://" + path, type);
		try
		{
			((DownloadHandlerAudioClip)req.downloadHandler).streamAudio = false;
			yield return req.SendWebRequest();
			if ((int)req.result == 1)
			{
				AudioClip content = DownloadHandlerAudioClip.GetContent(req);
				((Object)content).name = Path.GetFileNameWithoutExtension(path);
				target.Add(content);
			}
			else
			{
				Log.LogWarning((object)("[BiomeMusic] Load failed: " + path + " — " + req.error));
			}
		}
		finally
		{
			((IDisposable)req)?.Dispose();
		}
	}

	private IEnumerator MusicLoop()
	{
		yield return (object)new WaitForSeconds(20f);
		while (true)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				yield return (object)new WaitForSeconds(2f);
				continue;
			}
			string biomeKey = GetBiomeKey(localPlayer);
			if (_isFading)
			{
				yield return (object)new WaitForSeconds(0.1f);
				continue;
			}
			if (source.isPlaying)
			{
				if (biomeKey != _currentZone)
				{
					_saved[_currentZone] = (source.clip, source.time);
					_skipNextGap = true;
					Log.LogInfo((object)$"[BiomeMusic] [{_currentZone}→{biomeKey}] saved {source.time:F0}s — fading");
					((MonoBehaviour)this).StartCoroutine(FadeOutAndStop());
				}
				yield return (object)new WaitForSeconds(1f);
				continue;
			}
			if (!_skipNextGap)
			{
				float num = Random.Range(cfgMinGap.Value, cfgMaxGap.Value);
				yield return (object)new WaitForSeconds(num);
			}
			_skipNextGap = false;
			localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				continue;
			}
			biomeKey = GetBiomeKey(localPlayer);
			if (!clips.ContainsKey(biomeKey) || clips[biomeKey].Count == 0)
			{
				yield return (object)new WaitForSeconds(2f);
				continue;
			}
			float num2 = 0f;
			AudioClip val;
			if (_saved.TryGetValue(biomeKey, out (AudioClip, float) value) && (Object)(object)value.Item1 != (Object)null && value.Item1.length - value.Item2 > 10f)
			{
				(val, num2) = value;
				_saved.Remove(biomeKey);
				Log.LogInfo((object)$"[BiomeMusic] ↩ Resume [{biomeKey}] {((Object)val).name} from {num2:F0}s");
			}
			else
			{
				val = NextClip(biomeKey);
				if (_saved.ContainsKey(biomeKey))
				{
					_saved.Remove(biomeKey);
				}
				Log.LogInfo((object)$"[BiomeMusic] ▶ [{biomeKey}] {((Object)val).name} ({val.length:F0}s)");
			}
			_currentZone = biomeKey;
			source.clip = val;
			source.time = num2;
			_fade = 0f;
			source.Play();
			yield return ((MonoBehaviour)this).StartCoroutine(FadeIn());
		}
	}
}