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 Music Overhaul v1.0.5
plugins/TNSMusicOverhaul.dll
Decompiled a day 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.Configuration; 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(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("TNSMusicOverhaul")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("TNSMusicOverhaul")] [assembly: AssemblyTitle("TNSMusicOverhaul")] [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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace BiomeMusic { [BepInPlugin("TNS-Music-Overhaul", "TNS Music Overhaul", "1.0.5")] public class BiomeMusicPlugin : BaseUnityPlugin { public const string Guid = "TNS-Music-Overhaul"; public const string Name = "TNS Music Overhaul"; public const string Version = "1.0.5"; private const string DefaultRaidTiers = "army_eikthyr:1, army_theelder:1, foresttrolls:1, skeletons:1, blobs:2, army_bonemass:2, surtlings:2, wolves:3, army_moder:3, bats:3, army_goblin:4"; private const string VanillaHomeMusic = "home"; private const string VanillaSailingMusic = "sailing"; private const string OceanFolder = "Ocean"; private Harmony _harmony; private Dictionary<string, int> _raidTiers; private ConfigEntry<bool> _fallbackToClear; private ConfigEntry<string> _rainKeywords; private ConfigEntry<string> _stormKeywords; private ConfigEntry<float> _volume; private ConfigEntry<float> _fadeInTime; private ConfigEntry<string> _disabledBiomes; private ConfigEntry<bool> _sailingUsesOcean; private ConfigEntry<bool> _skipVanillaTransitions; private ConfigEntry<bool> _raidMusic; private ConfigEntry<string> _raidTierConfig; private ConfigEntry<bool> _logChanges; private ConfigEntry<float> _maxSilence; private ConfigEntry<string> _musicFolder; private string _lastLoggedSlot; private string _lastLoggedRaid; private bool _loggedSilence; internal static BiomeMusicPlugin Instance { get; private set; } internal MusicLibrary Library { get; private set; } private void Awake() { //IL_02c5: Unknown result type (might be due to invalid IL or missing references) //IL_02cf: Expected O, but got Unknown Instance = this; string text = Path.Combine(Paths.ConfigPath, "TNS-Music-Overhaul", "Music"); _musicFolder = ((BaseUnityPlugin)this).Config.Bind<string>("Music", "MusicFolder", text, "Where all the music lives. Move it anywhere you like, e.g. D:\\ValheimMusic, to keep it out of your mod manager profile, so profile exports and syncing stay small and several profiles can share one copy. Change this and the music, including songs you added yourself, moves to the new folder the next time you start the game. If the new folder already has music of its own, both are left as they are and the new one is used. If the folder can't be used at all, say a typo or a drive that isn't there, the music stays where it is and a warning goes in the log."); _fallbackToClear = ((BaseUnityPlugin)this).Config.Bind<bool>("Music", "FallbackToClear", true, "If a Rain or Thunderstorm folder is empty, play the Clear tracks for that biome and time of day instead of the vanilla music."); _volume = ((BaseUnityPlugin)this).Config.Bind<float>("Music", "Volume", 1f, "Volume of the custom tracks (0-1). Still scaled by the in-game music volume slider."); _fadeInTime = ((BaseUnityPlugin)this).Config.Bind<float>("Music", "FadeInTime", 3f, "Seconds to fade a custom track in."); _maxSilence = ((BaseUnityPlugin)this).Config.Bind<float>("Music", "MaxSilenceSeconds", 60f, "Longest the game waits before starting the next biome song, e.g. after you leave your base. Valheim picks a random wait each time; this caps it. 0 keeps Valheim's own waits."); _disabledBiomes = ((BaseUnityPlugin)this).Config.Bind<string>("Music", "DisabledBiomes", "AshLands,Mistlands", "Comma-separated biomes that always keep their vanilla music. Their folders are kept but not loaded. Names: " + string.Join(", ", MusicLibrary.Biomes)); _stormKeywords = ((BaseUnityPlugin)this).Config.Bind<string>("Weather", "ThunderstormKeywords", "thunder,storm,blizzard", "Comma-separated. A weather preset whose name contains any of these counts as Thunderstorm. Checked before Rain."); _rainKeywords = ((BaseUnityPlugin)this).Config.Bind<string>("Weather", "RainKeywords", "rain,snow", "Comma-separated. A weather preset whose name contains any of these, or that makes the player wet, counts as Rain."); _sailingUsesOcean = ((BaseUnityPlugin)this).Config.Bind<bool>("Sailing", "UseOceanMusic", true, "While sailing, play the Ocean folder for the current weather and time of day instead of the vanilla sailing music. Sailing in the Ashlands keeps its vanilla music."); _skipVanillaTransitions = ((BaseUnityPlugin)this).Config.Bind<bool>("Transitions", "SkipVanillaWhenEmpty", true, "The game plays a short piece at sunrise and sunset. Tracks in Music/Transition/Morning and Music/Transition/Evening replace it. If that folder is empty: true skips the vanilla piece, false keeps it."); _raidMusic = ((BaseUnityPlugin)this).Config.Bind<bool>("Raids", "Enabled", true, "Replace raid music with tracks from Music/Raids."); _raidTierConfig = ((BaseUnityPlugin)this).Config.Bind<string>("Raids", "Tiers", "army_eikthyr:1, army_theelder:1, foresttrolls:1, skeletons:1, blobs:2, army_bonemass:2, surtlings:2, wolves:3, army_moder:3, bats:3, army_goblin:4", "Comma-separated eventName:tier pairs (tier 1-4) choosing which Music/Raids/TierN folder a raid uses. A folder named after the event (e.g. Music/Raids/army_moder) overrides its tier. Unlisted raids keep vanilla music."); _logChanges = ((BaseUnityPlugin)this).Config.Bind<bool>("Troubleshooting", "LogChanges", true, "Log the biome, time, weather preset and chosen slot whenever they change, and each raid's event name. Useful for tuning the keywords and tiers."); DropStaleSettings(); _raidTiers = ParseRaidTiers(_raidTierConfig.Value); HashSet<string> disabledBiomes = new HashSet<string>(SplitList(_disabledBiomes.Value), StringComparer.OrdinalIgnoreCase); string shippedRoot = Path.Combine(Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location), "Music"); string path = Path.Combine(Paths.ConfigPath, "TNS-Music-Overhaul"); string recordPath = Path.Combine(path, "music-folder.txt"); string text2 = ResolveMusicFolder(text, recordPath); MusicMover mover = new MusicMover(text2, recordPath, ((BaseUnityPlugin)this).Logger); MusicInstaller installer = new MusicInstaller(shippedRoot, text2, Path.Combine(path, "installed-songs.txt"), ((BaseUnityPlugin)this).Logger); Library = new MusicLibrary(text2, ((BaseUnityPlugin)this).Logger); ((MonoBehaviour)this).StartCoroutine(SetUpThenLoad(mover, installer, disabledBiomes)); _harmony = new Harmony("TNS-Music-Overhaul"); _harmony.PatchAll(typeof(BiomeMusicPlugin).Assembly); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private string ResolveMusicFolder(string defaultFolder, string recordPath) { if (TryUseFolder(_musicFolder.Value, out var folder, out var problem)) { return folder; } string text = MusicMover.ReadRecordedFolder(recordPath, ((BaseUnityPlugin)this).Logger); if (text != null && TryUseFolder(text, out var folder2, out var _)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Can't use the music folder '" + _musicFolder.Value + "' (" + problem + "); keeping the music where it is, in " + folder2)); return folder2; } ((BaseUnityPlugin)this).Logger.LogWarning((object)("Can't use the music folder '" + _musicFolder.Value + "' (" + problem + "); using " + defaultFolder)); return defaultFolder; } private static bool TryUseFolder(string path, out string folder, out string problem) { folder = null; problem = "no folder set"; if (string.IsNullOrEmpty(path?.Trim())) { return false; } try { folder = Path.GetFullPath(path.Trim()); Directory.CreateDirectory(folder); return true; } catch (Exception ex) { problem = ex.Message; return false; } } private void DropStaleSettings() { try { if (AccessTools.Property(typeof(ConfigFile), "OrphanedEntries")?.GetValue(((BaseUnityPlugin)this).Config) is IDictionary { Count: not 0 } dictionary) { dictionary.Clear(); ((BaseUnityPlugin)this).Config.Save(); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not tidy up old settings: " + ex.Message)); } } private IEnumerator SetUpThenLoad(MusicMover mover, MusicInstaller installer, ICollection<string> disabledBiomes) { yield return ((MonoBehaviour)this).StartCoroutine(mover.MoveFromPreviousFolder()); Library.EnsureFolders(); yield return ((MonoBehaviour)this).StartCoroutine(installer.Install()); yield return ((MonoBehaviour)this).StartCoroutine(Library.Load(_volume.Value, _fadeInTime.Value, disabledBiomes)); } internal NamedMusic PickBiomeMusic(NamedMusic vanilla) { //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_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) if (!Library.IsLoaded) { return null; } EnvMan instance = EnvMan.instance; Player localPlayer = Player.m_localPlayer; if ((Object)(object)instance == (Object)null || (Object)(object)localPlayer == (Object)null) { return null; } string text; if (vanilla == null) { if (((Character)localPlayer).InInterior()) { return null; } text = ((object)instance.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString(); } else if (vanilla.m_name == "home") { text = "HomeRest"; } else { if (!(vanilla.m_name == instance.GetAmbientMusic())) { return null; } text = ((object)instance.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString(); } TimeOfDay timeOfDay = ((!EnvMan.IsDay()) ? TimeOfDay.Night : TimeOfDay.Day); EnvSetup currentEnvironment = instance.GetCurrentEnvironment(); Weather weather = Classify(currentEnvironment); NamedMusic val = Library.FindBiome(text, timeOfDay, weather, _fallbackToClear.Value); if (val != null) { val.m_loop = vanilla?.m_loop ?? false; if (vanilla != null) { val.m_resume = vanilla.m_resume; } } if (_logChanges.Value) { string text2 = $"{text}/{timeOfDay}/{weather} (weather preset '{currentEnvironment?.m_name}', " + "vanilla '" + (vanilla?.m_name ?? "none") + "')"; if (text2 != _lastLoggedSlot) { _lastLoggedSlot = text2; ((BaseUnityPlugin)this).Logger.LogInfo((object)(text2 + " -> " + ((val != null) ? val.m_name : "vanilla music"))); } } return val; } internal void OnStartMusic(NamedMusic music) { if (Library != null && music.m_name.StartsWith("BiomeMusic_")) { string text = Library.PrepareToStart(music); if (text != null && _logChanges.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)((music.m_name.EndsWith("/sailing") ? "Sailing, playing" : "Playing") + " '" + text + "' from " + music.m_name)); } } } internal void CapSilence(MusicMan musicMan, ref float interval) { if (!_loggedSilence) { _loggedSilence = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)($"Vanilla waits {musicMan.m_randomMusicIntervalMin:0}-{musicMan.m_randomMusicIntervalMax:0} s " + $"between biome songs; MaxSilenceSeconds = {_maxSilence.Value:0} (0 = vanilla)")); } float value = _maxSilence.Value; if (value > 0f && interval > value) { interval = value; } } internal string PickSailingMusic(string vanilla) { if (vanilla != "sailing" || !_sailingUsesOcean.Value || !Library.IsLoaded) { return null; } EnvMan instance = EnvMan.instance; MusicMan instance2 = MusicMan.instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance2 == (Object)null) { return null; } TimeOfDay time = ((!EnvMan.IsDay()) ? TimeOfDay.Night : TimeOfDay.Day); EnvSetup currentEnvironment = instance.GetCurrentEnvironment(); Weather weather = Classify(currentEnvironment); NamedMusic val = Library.FindBiome("Ocean", time, weather, _fallbackToClear.Value); if (val == null) { return null; } NamedMusic variant = Library.GetVariant(val, "sailing"); NamedMusic val2 = instance2.m_music.Find((NamedMusic m) => m.m_name == vanilla); if (val2 != null) { variant.m_loop = val2.m_loop; variant.m_resume = val2.m_resume; } return variant.m_name; } internal bool OnTriggerMusic(ref string name) { if (!MusicLibrary.IsTransition(name) || !Library.IsLoaded) { return true; } NamedMusic val = Library.FindTransition(name); bool flag = val != null || !_skipVanillaTransitions.Value; if (_logChanges.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Transition '" + name + "' -> " + ((val != null) ? val.m_name : (flag ? "vanilla music" : "skipped")))); } if (val != null) { name = val.m_name; } return flag; } internal string PickRaidMusic(RandomEvent raid) { if (raid == null || !_raidMusic.Value || !Library.IsLoaded) { return null; } int value; int? num = (_raidTiers.TryGetValue(raid.m_name, out value) ? new int?(value) : ((int?)null)); NamedMusic val = Library.FindRaid(raid.m_name, num); if (_logChanges.Value && raid.m_name != _lastLoggedRaid) { _lastLoggedRaid = raid.m_name; ((BaseUnityPlugin)this).Logger.LogInfo((object)("Raid '" + raid.m_name + "' (tier " + (num?.ToString() ?? "none") + ", vanilla '" + raid.m_forceMusic + "') -> " + ((val != null) ? val.m_name : "vanilla music"))); } return val?.m_name; } private Weather Classify(EnvSetup env) { if (env == null) { return Weather.Clear; } string name = env.m_name ?? ""; if (Matches(name, _stormKeywords.Value)) { return Weather.Thunderstorm; } if (Matches(name, _rainKeywords.Value) || env.m_isWet) { return Weather.Rain; } return Weather.Clear; } private Dictionary<string, int> ParseRaidTiers(string value) { Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); foreach (string item in SplitList(value)) { string[] array = item.Split(':'); if (array.Length == 2 && int.TryParse(array[1].Trim(), out var result) && result >= 1 && result <= 4) { dictionary[array[0].Trim()] = result; } else { ((BaseUnityPlugin)this).Logger.LogWarning((object)$"Ignoring raid tier entry '{item}'; expected eventName:1-{4}"); } } return dictionary; } private static IEnumerable<string> SplitList(string value) { return from s in value.Split(',') select s.Trim() into s where s.Length > 0 select s; } private static bool Matches(string name, string keywords) { return SplitList(keywords).Any((string k) => name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0); } } [HarmonyPatch(typeof(MusicMan), "GetEnvironmentMusic")] internal static class GetEnvironmentMusicPatch { private static void Postfix(ref NamedMusic __result) { NamedMusic val = BiomeMusicPlugin.Instance?.PickBiomeMusic(__result); if (val != null) { __result = val; } } } [HarmonyPatch(typeof(RandEventSystem), "GetMusicOverride")] internal static class GetMusicOverridePatch { private static void Postfix(RandEventSystem __instance, ref string __result) { if (!string.IsNullOrEmpty(__result)) { string text = BiomeMusicPlugin.Instance?.PickRaidMusic(__instance.GetActiveEvent()); if (text != null) { __result = text; } } } } [HarmonyPatch(typeof(MusicMan), "StartMusic", new Type[] { typeof(NamedMusic) })] internal static class StartMusicPatch { private static readonly FieldRef<MusicMan, NamedMusic> CurrentMusic = AccessTools.FieldRefAccess<MusicMan, NamedMusic>("m_currentMusic"); private static readonly FieldRef<MusicMan, NamedMusic> QueuedMusic = AccessTools.FieldRefAccess<MusicMan, NamedMusic>("m_queuedMusic"); private static void Prefix(MusicMan __instance, NamedMusic music) { if (music != null && music != CurrentMusic.Invoke(__instance) && music != QueuedMusic.Invoke(__instance)) { BiomeMusicPlugin.Instance?.OnStartMusic(music); } } } [HarmonyPatch(typeof(MusicMan), "HandleEnvironmentMusic")] internal static class HandleEnvironmentMusicPatch { private static readonly FieldRef<MusicMan, float> RandomAmbientInterval = AccessTools.FieldRefAccess<MusicMan, float>("m_randomAmbientInterval"); private static void Prefix(MusicMan __instance) { BiomeMusicPlugin.Instance?.CapSilence(__instance, ref RandomAmbientInterval.Invoke(__instance)); } } [HarmonyPatch(typeof(MusicMan), "GetSailingMusic")] internal static class GetSailingMusicPatch { private static void Postfix(ref string __result) { string text = BiomeMusicPlugin.Instance?.PickSailingMusic(__result); if (text != null) { __result = text; } } } [HarmonyPatch(typeof(MusicMan), "TriggerMusic")] internal static class TriggerMusicPatch { private static bool Prefix(ref string name) { return BiomeMusicPlugin.Instance?.OnTriggerMusic(ref name) ?? true; } } [HarmonyPatch(typeof(MusicMan), "FindMusic")] internal static class FindMusicPatch { private static bool Prefix(string musicName, ref NamedMusic __result) { if (musicName == null || !musicName.StartsWith("BiomeMusic_")) { return true; } __result = BiomeMusicPlugin.Instance?.Library?.FindByName(musicName); return __result == null; } } internal class MusicInstaller { private static readonly string[] Extensions = new string[2] { ".ogg", ".wav" }; private readonly string _shippedRoot; private readonly string _userRoot; private readonly string _recordPath; private readonly ManualLogSource _log; public MusicInstaller(string shippedRoot, string userRoot, string recordPath, ManualLogSource log) { _shippedRoot = shippedRoot; _userRoot = userRoot; _recordPath = recordPath; _log = log; } public IEnumerator Install() { if (!Directory.Exists(_shippedRoot)) { yield break; } List<string> list = (from f in Directory.GetFiles(_shippedRoot, "*", SearchOption.AllDirectories) where Extensions.Contains(Path.GetExtension(f).ToLowerInvariant()) select f).OrderBy<string, string>((string f) => f, StringComparer.OrdinalIgnoreCase).ToList(); if (list.Count == 0) { yield break; } HashSet<string> installed = ReadRecord(); int copied = 0; int failed = 0; Stopwatch watch = Stopwatch.StartNew(); foreach (string item in list) { string text = item.Substring(_shippedRoot.Length).TrimStart(Path.DirectorySeparatorChar); string text2 = Path.Combine(_userRoot, text); if (!installed.Contains(text) && !File.Exists(text2)) { try { Directory.CreateDirectory(Path.GetDirectoryName(text2)); File.Copy(item, text2, overwrite: false); copied++; } catch (Exception ex) { _log.LogWarning((object)("Could not install " + text + ": " + ex.Message)); failed++; continue; } } installed.Add(text); try { File.Delete(item); } catch (Exception ex2) { _log.LogWarning((object)("Could not remove the mod's copy of " + text + ": " + ex2.Message)); } yield return null; } WriteRecord(installed); if (failed == 0) { try { Directory.Delete(_shippedRoot, recursive: true); } catch (Exception ex3) { _log.LogWarning((object)("Could not remove " + _shippedRoot + ": " + ex3.Message)); } } if (copied > 0 || failed > 0) { _log.LogInfo((object)($"Installed {copied} song(s) into {_userRoot} in {watch.Elapsed.TotalSeconds:0.0} s" + ((failed > 0) ? $" ({failed} failed)" : ""))); } } private HashSet<string> ReadRecord() { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (!File.Exists(_recordPath)) { return hashSet; } if (!Directory.Exists(_userRoot) || !Directory.GetFiles(_userRoot, "*", SearchOption.AllDirectories).Any((string f) => Extensions.Contains(Path.GetExtension(f).ToLowerInvariant()))) { _log.LogInfo((object)"No songs found in the music folder; installing the mod's songs again."); return hashSet; } try { string[] array = File.ReadAllLines(_recordPath); foreach (string text in array) { if (text.Length > 0 && !text.StartsWith("#")) { hashSet.Add(text.Trim()); } } } catch (Exception ex) { _log.LogWarning((object)("Could not read " + _recordPath + ": " + ex.Message)); } return hashSet; } private void WriteRecord(IEnumerable<string> installed) { try { Directory.CreateDirectory(Path.GetDirectoryName(_recordPath)); File.WriteAllLines(_recordPath, new string[6] { "# Songs TNS Music Overhaul has already copied into its music folder.", "# Listed songs are not copied again, so ones you delete stay deleted.", "#", "# To get deleted songs back, reinstall the mod in your mod manager and start", "# the game: that is the only time the mod has its own songs to hand out again.", "# Removing lines here (or this whole file) decides what it hands back then." }.Concat(installed.OrderBy<string, string>((string p) => p, StringComparer.OrdinalIgnoreCase))); } catch (Exception ex) { _log.LogWarning((object)("Could not write " + _recordPath + ": " + ex.Message)); } } } internal enum TimeOfDay { Day, Night } internal enum Weather { Clear, Rain, Thunderstorm } internal class MusicLibrary { public static readonly string[] Biomes = new string[9] { "Meadows", "BlackForest", "Swamp", "Mountain", "Plains", "Mistlands", "AshLands", "DeepNorth", "Ocean" }; private static readonly HashSet<string> SnowyBiomes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Mountain", "DeepNorth" }; public const string HomeFolder = "HomeRest"; public const string RaidsFolder = "Raids"; public const string TransitionFolder = "Transition"; public static readonly string[] Transitions = new string[2] { "Morning", "Evening" }; public const int RaidTiers = 4; public const string NamePrefix = "BiomeMusic_"; private static readonly string[] Extensions = new string[2] { ".ogg", ".wav" }; private readonly Dictionary<string, NamedMusic> _tracks = new Dictionary<string, NamedMusic>(); private readonly Dictionary<string, NamedMusic> _byName = new Dictionary<string, NamedMusic>(); private readonly Dictionary<string, AudioClip[]> _pools = new Dictionary<string, AudioClip[]>(); private readonly Dictionary<string, Queue<int>> _shuffles = new Dictionary<string, Queue<int>>(); private readonly Dictionary<string, int> _lastPlayed = new Dictionary<string, int>(); private readonly Random _random = new Random(); private readonly string _root; private readonly ManualLogSource _log; private int _clipCount; private double _mainThreadMs; private double _slowestClipMs; public bool IsLoaded { get; private set; } public MusicLibrary(string root, ManualLogSource log) { _root = root; _log = log; } public void EnsureFolders() { foreach (string item in Biomes.Append("HomeRest")) { foreach (TimeOfDay value in Enum.GetValues(typeof(TimeOfDay))) { foreach (Weather value2 in Enum.GetValues(typeof(Weather))) { string[] source = SlotFolders(value, value2); string path = (SnowyBiomes.Contains(item) ? source.Last() : source.First()); Directory.CreateDirectory(Path.Combine(_root, item, path)); } } } for (int i = 1; i <= 4; i++) { Directory.CreateDirectory(Path.Combine(_root, "Raids", "Tier" + i)); } string[] transitions = Transitions; foreach (string path2 in transitions) { Directory.CreateDirectory(Path.Combine(_root, "Transition", path2)); } string path3 = Path.Combine(_root, "READ ME.txt"); if (!File.Exists(path3)) { File.WriteAllText(path3, string.Join(Environment.NewLine, "All music for TNS Music Overhaul lives here, and SURVIVES MOD UPDATES.", "", "The songs that come with the mod are copied here the first time you run it, and", "updates add any new ones. Everything else is yours to change:", "", " - Add songs: drop .ogg or .wav files into the folder for the moment you want", " them to play, for example Meadows\\ClearNight, then restart the game.", " One song from that folder is picked at random each time.", " - Remove songs: delete the files. They stay gone; updates won't bring them back.", " - Empty folder: that moment falls back to the same time of day's clear-weather", " songs, or to Valheim's own music if there are none.", " - Want the mod's songs back? Reinstall the mod in your mod manager, then", " start the game. This folder is the only copy once the mod has run.", "", "To silence a whole biome instead, add it to DisabledBiomes in", "TNS-Music-Overhaul.cfg.")); } } public IEnumerator Load(float volume, float fadeInTime, ICollection<string> disabledBiomes) { Stopwatch total = Stopwatch.StartNew(); foreach (string biome in from b in Biomes.Append("HomeRest") where !disabledBiomes.Contains(b) select b) { foreach (TimeOfDay time in Enum.GetValues(typeof(TimeOfDay))) { foreach (Weather value in Enum.GetValues(typeof(Weather))) { IEnumerable<string> dirs = from name in SlotFolders(time, value) select Path.Combine(_root, biome, name); yield return LoadSlot(dirs, BiomeKey(biome, time, value), volume, fadeInTime, loop: false); } } } string path = Path.Combine(_root, "Raids"); string[] directories; if (Directory.Exists(path)) { directories = Directory.GetDirectories(path); foreach (string text in directories) { yield return LoadSlot(text, RaidKey(Path.GetFileName(text)), volume, fadeInTime, loop: true); } } directories = Transitions; foreach (string text2 in directories) { yield return LoadSlot(Path.Combine(_root, "Transition", text2), TransitionKey(text2), volume, fadeInTime, loop: false); } IsLoaded = true; _log.LogInfo((object)($"Loaded {_clipCount} track(s) into {_tracks.Count} slot(s) from {_root} " + $"in {total.Elapsed.TotalSeconds:0.0} s (main thread {_mainThreadMs:0} ms, slowest track {_slowestClipMs:0} ms)")); } public NamedMusic FindBiome(string biome, TimeOfDay time, Weather weather, bool fallbackToClear) { if (_tracks.TryGetValue(BiomeKey(biome, time, weather), out var value)) { return value; } if (fallbackToClear && weather != Weather.Clear && _tracks.TryGetValue(BiomeKey(biome, time, Weather.Clear), out value)) { return value; } return null; } public NamedMusic FindRaid(string eventName, int? tier) { if (_tracks.TryGetValue(RaidKey(eventName), out var value)) { return value; } if (tier.HasValue && _tracks.TryGetValue(RaidKey("Tier" + tier.Value), out value)) { return value; } return null; } public NamedMusic FindTransition(string vanillaName) { if (!_tracks.TryGetValue(TransitionKey(vanillaName), out var value)) { return null; } return value; } public static bool IsTransition(string vanillaName) { return Transitions.Any((string t) => t.Equals(vanillaName, StringComparison.OrdinalIgnoreCase)); } public NamedMusic GetVariant(NamedMusic source, string suffix) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003b: 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_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0085: 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_0099: Expected O, but got Unknown string text = source.m_name + "/" + suffix; if (_byName.TryGetValue(text, out var value)) { return value; } AudioClip[] array = _pools[source.m_name]; value = new NamedMusic { m_name = text, m_clips = array, m_volume = source.m_volume, m_fadeInTime = source.m_fadeInTime, m_alwaysFadeout = source.m_alwaysFadeout, m_loop = source.m_loop, m_resume = source.m_resume, m_enabled = true, m_ambientMusic = source.m_ambientMusic }; _byName[text] = value; _pools[text] = array; return value; } public string PrepareToStart(NamedMusic music) { if (!_pools.TryGetValue(music.m_name, out var value) || value.Length < 2) { return null; } AudioClip val = ((music.m_clips != null && music.m_clips.Length == 1) ? music.m_clips[0] : null); if (music.m_resume && (Object)(object)val != (Object)null && Time.time - music.m_lastPlayedTime < val.length) { return null; } if (!_shuffles.TryGetValue(music.m_name, out var value2) || value2.Count == 0) { value2 = Shuffle(value.Length, _lastPlayed.TryGetValue(music.m_name, out var value3) ? value3 : (-1)); _shuffles[music.m_name] = value2; } int num = value2.Dequeue(); _lastPlayed[music.m_name] = num; music.m_clips = (AudioClip[])(object)new AudioClip[1] { value[num] }; return ((Object)value[num]).name; } private Queue<int> Shuffle(int count, int justPlayed) { int[] array = Enumerable.Range(0, count).ToArray(); for (int num = count - 1; num > 0; num--) { int num2 = _random.Next(num + 1); ref int reference = ref array[num]; ref int reference2 = ref array[num2]; int num3 = array[num2]; int num4 = array[num]; reference = num3; reference2 = num4; } if (array[0] == justPlayed) { ref int reference2 = ref array[0]; ref int reference3 = ref array[count - 1]; int num4 = array[count - 1]; int num3 = array[0]; reference2 = num4; reference3 = num3; } return new Queue<int>(array); } public NamedMusic FindByName(string name) { if (name == null || !_byName.TryGetValue(name, out var value)) { return null; } return value; } private IEnumerator LoadSlot(string dir, string key, float volume, float fadeInTime, bool loop) { return LoadSlot(new string[1] { dir }, key, volume, fadeInTime, loop); } private IEnumerator LoadSlot(IEnumerable<string> dirs, string key, float volume, float fadeInTime, bool loop) { List<string> list = (from f in dirs.Where(Directory.Exists).SelectMany(Directory.GetFiles) where Extensions.Contains(Path.GetExtension(f).ToLowerInvariant()) select f).OrderBy<string, string>(Path.GetFileName, StringComparer.OrdinalIgnoreCase).ThenBy<string, string>((string f) => f, StringComparer.OrdinalIgnoreCase).ToList(); if (list.Count == 0) { yield break; } List<AudioClip> clips = new List<AudioClip>(); foreach (string item in list) { yield return LoadClip(item, clips); } if (clips.Count != 0) { NamedMusic val = new NamedMusic { m_name = "BiomeMusic_" + key, m_clips = clips.ToArray(), m_volume = volume, m_fadeInTime = fadeInTime, m_alwaysFadeout = true, m_loop = loop, m_resume = false, m_enabled = true, m_ambientMusic = !loop }; _tracks[key] = val; _byName[val.m_name] = val; _pools[val.m_name] = val.m_clips; _clipCount += clips.Count; _log.LogInfo((object)$"{key}: {clips.Count} track(s)"); } } private IEnumerator LoadClip(string path, List<AudioClip> into) { AudioType val = (AudioType)((Path.GetExtension(path).ToLowerInvariant() == ".wav") ? 20 : 14); UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(new Uri(path).AbsoluteUri, val); try { ((DownloadHandlerAudioClip)request.downloadHandler).streamAudio = true; yield return request.SendWebRequest(); if ((int)request.result != 1) { _log.LogWarning((object)("Could not load " + path + ": " + request.error)); yield break; } Stopwatch stopwatch = Stopwatch.StartNew(); AudioClip content = DownloadHandlerAudioClip.GetContent(request); stopwatch.Stop(); _mainThreadMs += stopwatch.Elapsed.TotalMilliseconds; _slowestClipMs = Math.Max(_slowestClipMs, stopwatch.Elapsed.TotalMilliseconds); ((Object)content).name = Path.GetFileNameWithoutExtension(path); into.Add(content); } finally { ((IDisposable)request)?.Dispose(); } } private static string[] SlotFolders(TimeOfDay time, Weather weather) { return weather switch { Weather.Rain => new string[2] { "Rainy" + time, "Snow" + time }, Weather.Thunderstorm => new string[2] { "Thunderstorm" + time, "Blizzard" + time }, _ => new string[1] { "Clear" + time }, }; } private static string BiomeKey(string biome, TimeOfDay time, Weather weather) { return $"{biome}/{time}/{weather}".ToLowerInvariant(); } private static string RaidKey(string folder) { return ("Raids/" + folder).ToLowerInvariant(); } private static string TransitionKey(string name) { return ("Transition/" + name).ToLowerInvariant(); } } internal class MusicMover { private static readonly string[] Extensions = new string[2] { ".ogg", ".wav" }; private readonly string _currentRoot; private readonly string _recordPath; private readonly ManualLogSource _log; public MusicMover(string currentRoot, string recordPath, ManualLogSource log) { _currentRoot = currentRoot; _recordPath = recordPath; _log = log; } public IEnumerator MoveFromPreviousFolder() { string text = ReadPreviousFolder(); if (text != null && !SameFolder(text, _currentRoot) && HasMusic(text) && !HasMusic(_currentRoot)) { yield return Move(text); } RememberCurrentFolder(); } private IEnumerator Move(string previous) { Stopwatch watch = Stopwatch.StartNew(); int moved = 0; int failed = 0; string[] files = Directory.GetFiles(previous, "*", SearchOption.AllDirectories); foreach (string text in files) { string text2 = text.Substring(previous.Length).TrimStart(Path.DirectorySeparatorChar); string text3 = Path.Combine(_currentRoot, text2); try { if (!File.Exists(text3)) { Directory.CreateDirectory(Path.GetDirectoryName(text3)); File.Copy(text, text3, overwrite: false); moved++; } File.Delete(text); } catch (Exception ex) { _log.LogWarning((object)("Could not move " + text2 + ": " + ex.Message)); failed++; } yield return null; } if (failed == 0) { try { Directory.Delete(previous, recursive: true); } catch (Exception ex2) { _log.LogWarning((object)("Could not remove the old music folder " + previous + ": " + ex2.Message)); } } _log.LogInfo((object)($"Moved {moved} file(s) from {previous} to {_currentRoot} in {watch.Elapsed.TotalSeconds:0.0} s" + ((failed > 0) ? $"; {failed} could not be moved and are still in the old folder" : ""))); } private string ReadPreviousFolder() { return ReadRecordedFolder(_recordPath, _log); } public static string ReadRecordedFolder(string recordPath, ManualLogSource log) { try { if (!File.Exists(recordPath)) { return null; } string text = File.ReadAllLines(recordPath).FirstOrDefault((string l) => l.Length > 0 && !l.StartsWith("#"))?.Trim(); return string.IsNullOrEmpty(text) ? null : text; } catch (Exception ex) { log.LogWarning((object)("Could not read " + recordPath + ": " + ex.Message)); return null; } } private void RememberCurrentFolder() { try { if (!SameFolder(ReadPreviousFolder(), _currentRoot)) { Directory.CreateDirectory(Path.GetDirectoryName(_recordPath)); File.WriteAllLines(_recordPath, new string[3] { "# The music folder TNS Music Overhaul is using. Change MusicFolder in", "# TNS-Music-Overhaul.cfg to move the music; this file only records where it went.", _currentRoot }); } } catch (Exception ex) { _log.LogWarning((object)("Could not write " + _recordPath + ": " + ex.Message)); } } private static bool SameFolder(string a, string b) { if (a != null && b != null) { return string.Equals(a.TrimEnd(Path.DirectorySeparatorChar), b.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase); } return false; } private static bool HasMusic(string folder) { if (Directory.Exists(folder)) { return Directory.GetFiles(folder, "*", SearchOption.AllDirectories).Any((string f) => Extensions.Contains(Path.GetExtension(f).ToLowerInvariant())); } return false; } } }