Decompiled source of MapInstaller v0.0.2

MapInstaller/MapInstaller.dll

Decompiled 9 months ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Logging;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("MapInstaller")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+41dac237b6879bddb8198a3e00d04e82f3f72c6c")]
[assembly: AssemblyProduct("MapInstaller")]
[assembly: AssemblyTitle("MapInstaller")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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 MapInstaller
{
	internal class Installer
	{
		private static char _S = Path.DirectorySeparatorChar;

		private static string GAME_PATH = Path.GetDirectoryName(Application.dataPath);

		private static string BEPINEX_PATH = Path.Combine(GAME_PATH, $"BepInEx{_S}plugins");

		private static string MAPS_PATH = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), $"AppData{_S}LocalLow{_S}Colossal Order{_S}Cities Skylines II{_S}Maps");

		private static string THUNDERSTORE_PATH = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), $"AppData{_S}Roaming{_S}Thunderstore Mod Manager{_S}DataFolder{_S}CitiesSkylines2{_S}profiles");

		private static string RMODMAN_PATH = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), $"AppData{_S}Roaming{_S}r2modmanPlus-local{_S}CitiesSkylines2{_S}profiles");

		private static List<Action> _currentActions = new List<Action>();

		private static ManualLogSource _logger;

		private static bool _hasErrors = false;

		internal Installer(ManualLogSource logger)
		{
			_logger = logger;
		}

		private void ScanDirectory()
		{
			try
			{
				if (Directory.Exists(BEPINEX_PATH))
				{
					_logger.LogInfo((object)"Scanning BepInEx folder...");
					ProcessSource(BEPINEX_PATH);
				}
				string activeThunderstoreProfile = GetActiveThunderstoreProfile();
				if (!string.IsNullOrEmpty(activeThunderstoreProfile))
				{
					_logger.LogInfo((object)("Scanning Thunderstore folder '" + activeThunderstoreProfile + "'..."));
					ProcessSource(activeThunderstoreProfile);
				}
				string activeRModManProfile = GetActiveRModManProfile();
				if (!string.IsNullOrEmpty(activeRModManProfile))
				{
					_logger.LogInfo((object)("Scanning rModMan folder '" + activeRModManProfile + "'..."));
					ProcessSource(activeRModManProfile);
				}
				if (_currentActions.Count == 0)
				{
					OnComplete();
					_logger.LogInfo((object)"No changes detected!");
				}
			}
			catch (Exception ex)
			{
				HandleException(ex);
			}
		}

		private string GetActiveProfile(string path)
		{
			if (!Directory.Exists(path))
			{
				return null;
			}
			DateTime dateTime = DateTime.MinValue;
			string result = string.Empty;
			string[] directories = Directory.GetDirectories(path);
			foreach (string path2 in directories)
			{
				string text = Path.Combine(path2, $"BepInEx{_S}plugins");
				if (Directory.Exists(text))
				{
					DateTime mostRecentModifiedDate = GetMostRecentModifiedDate(text);
					if (mostRecentModifiedDate > dateTime)
					{
						dateTime = mostRecentModifiedDate;
						result = text;
					}
				}
			}
			return result;
		}

		private string GetActiveThunderstoreProfile()
		{
			return GetActiveProfile(THUNDERSTORE_PATH);
		}

		private string GetActiveRModManProfile()
		{
			if (Directory.Exists(RMODMAN_PATH))
			{
				return GetActiveProfile(RMODMAN_PATH);
			}
			string environmentVariable = Environment.GetEnvironmentVariable("DOORSTOP_INVOKE_DLL_PATH");
			string directoryName = Path.GetDirectoryName(environmentVariable);
			string fullPath = Path.GetFullPath(Path.Combine(directoryName, "..", "..", ".."));
			return GetActiveProfile(fullPath);
		}

		public DateTime GetMostRecentModifiedDate(string directory)
		{
			return (from file in Directory.GetFiles(directory, "*", SearchOption.AllDirectories)
				select new FileInfo(file).LastWriteTime into date
				orderby date descending
				select date).FirstOrDefault();
		}

		private void ProcessSource(string sourceDirectory)
		{
			string[] directories = Directory.GetDirectories(sourceDirectory);
			foreach (string directory in directories)
			{
				ProcessDirectory(sourceDirectory, directory);
			}
			string[] files = Directory.GetFiles(sourceDirectory, "*.zip", SearchOption.AllDirectories);
			foreach (string zipFilePath in files)
			{
				ProcessZipFile(sourceDirectory, zipFilePath);
			}
		}

		private void EnsureMapsFolder()
		{
			try
			{
				Directory.CreateDirectory(MAPS_PATH);
			}
			catch (Exception ex)
			{
				HandleException(ex);
			}
		}

		private void ProcessDirectory(string sourceDirectory, string directory)
		{
			if (CheckDirectoryForMaps(directory, out var outputFolder) && FolderHasChanges(outputFolder, MAPS_PATH))
			{
				string relativePath = Path.GetRelativePath(sourceDirectory, outputFolder);
				_logger.LogInfo((object)("Detected changes at '" + relativePath + "', queuing for copy..."));
				_currentActions.Add(GenerateDirectoryCopyTask(sourceDirectory, outputFolder));
			}
		}

		private bool CheckDirectoryForMaps(string directory, out string outputFolder)
		{
			outputFolder = null;
			string text = Path.Combine(directory, "Maps");
			if (Directory.Exists(text))
			{
				outputFolder = text;
				return true;
			}
			string[] files = Directory.GetFiles(directory, "*.cok.cid");
			if (files.Length == 0)
			{
				return false;
			}
			string[] array = files;
			foreach (string path in array)
			{
				string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
				string path2 = Path.Combine(Path.GetDirectoryName(path), fileNameWithoutExtension);
				if (File.Exists(path2))
				{
					outputFolder = directory;
					return true;
				}
			}
			return false;
		}

		private void ProcessZipFile(string sourceDirectory, string zipFilePath)
		{
			string relativePath = Path.GetRelativePath(sourceDirectory, zipFilePath);
			if (ZipFileHasChanges(zipFilePath, MAPS_PATH))
			{
				_logger.LogInfo((object)("Detected changes in map ZIP '" + relativePath + "', queuing for copy..."));
				_currentActions.Add(GenerateZipCopyTask(sourceDirectory, zipFilePath));
			}
		}

		private void CopyFile(string file, string targetFolder)
		{
			try
			{
				string fileName = Path.GetFileName(file);
				string destFileName = Path.Combine(targetFolder, fileName);
				File.Copy(file, destFileName, overwrite: true);
			}
			catch (Exception ex)
			{
				HandleException(ex);
			}
		}

		private bool FolderHasChanges(string sourceFolder, string targetFolder)
		{
			string[] files = Directory.GetFiles(sourceFolder, "*.cok.cid");
			string[] files2 = Directory.GetFiles(sourceFolder, "*.cok");
			if (files.Length == 0 || files2.Length == 0)
			{
				return false;
			}
			List<string> list = files.ToList();
			list.AddRange(files2);
			foreach (string item in list)
			{
				string fileName = Path.GetFileName(item);
				string text = Path.Combine(targetFolder, fileName);
				if (!File.Exists(text))
				{
					return true;
				}
				if (GetFileHash(item) != GetFileHash(text))
				{
					return true;
				}
			}
			return false;
		}

		private bool ZipFileHasChanges(string zipFilePath, string targetFolder)
		{
			using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
			{
				foreach (ZipArchiveEntry entry in zipArchive.Entries)
				{
					if (!entry.FullName.EndsWith("/") && entry.FullName.ToLowerInvariant().Contains("maps/") && entry.FullName.ToLowerInvariant().EndsWith(".cok") && !entry.FullName.ToLowerInvariant().EndsWith(".cok.cid"))
					{
						string text = SanitiseZipEntryPath(Path.GetFileName(entry.FullName), MAPS_PATH);
						if (!File.Exists(text))
						{
							return true;
						}
						if (GetZipEntryHash(entry) != GetFileHash(text))
						{
							return true;
						}
					}
				}
			}
			return false;
		}

		private string GetHash(Stream stream)
		{
			using MD5 mD = MD5.Create();
			byte[] array = mD.ComputeHash(stream);
			return BitConverter.ToString(array).Replace("-", "").ToLowerInvariant();
		}

		private string GetFileHash(string file)
		{
			using FileStream stream = File.OpenRead(file);
			return GetHash(stream);
		}

		private string GetZipEntryHash(ZipArchiveEntry entry)
		{
			using Stream stream = entry.Open();
			return GetHash(stream);
		}

		private string SanitiseZipEntryPath(string entryFullName, string targetDirectory)
		{
			string fullPath = Path.GetFullPath(Path.Combine(targetDirectory, entryFullName));
			if (!fullPath.StartsWith(targetDirectory, StringComparison.OrdinalIgnoreCase))
			{
				throw new SecurityException("Attempted to extract a file outside of the target directory.");
			}
			return fullPath;
		}

		private Action GenerateDirectoryCopyTask(string sourceFolder, string copyFolder)
		{
			return delegate
			{
				try
				{
					string relativePath = Path.GetRelativePath(sourceFolder, copyFolder);
					string[] files = Directory.GetFiles(copyFolder, "*.cok.cid");
					string[] files2 = Directory.GetFiles(copyFolder, "*.cok");
					if (files.Length != 0 || files2.Length != 0)
					{
						List<string> list = files.ToList();
						list.AddRange(files2);
						_logger.LogInfo((object)$"Copying '{list.Count}' from '{relativePath}'.");
						int num = 0;
						int num2 = 0;
						foreach (string item in list)
						{
							if (num % 10 == 0)
							{
								_logger.LogInfo((object)$"Copying file {num2 + 1}/{list.Count}...");
							}
							CopyFile(item, MAPS_PATH);
							num2++;
							num = (int)((decimal)num2 / (decimal)list.Count * 100m);
						}
						_logger.LogInfo((object)("Finished copying '" + relativePath + "'."));
					}
				}
				catch (Exception ex)
				{
					HandleException(ex);
				}
			};
		}

		private Action GenerateZipCopyTask(string sourceDirectory, string zipFilePath)
		{
			return delegate
			{
				try
				{
					string relativePath = Path.GetRelativePath(sourceDirectory, zipFilePath);
					_logger.LogInfo((object)("Processing zip file '" + relativePath + "'."));
					using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
					{
						List<ZipArchiveEntry> list = zipArchive.Entries.Where((ZipArchiveEntry entry) => entry.FullName.ToLower().Contains("maps/") && (entry.FullName.ToLowerInvariant().EndsWith(".cok") || entry.FullName.ToLowerInvariant().EndsWith(".cok.cid"))).ToList();
						int count = list.Count;
						if (count == 0)
						{
							return;
						}
						_logger.LogInfo((object)$"Extracting '{count}' files from '{relativePath}'.");
						int num = 0;
						foreach (ZipArchiveEntry item in list)
						{
							string text = SanitiseZipEntryPath(Path.GetFileName(item.FullName), MAPS_PATH);
							Directory.CreateDirectory(Path.GetDirectoryName(text));
							item.ExtractToFile(text, overwrite: true);
							num++;
							int num2 = (int)((decimal)num / (decimal)count * 100m);
							if (num2 % 10 == 0)
							{
								_logger.LogInfo((object)$"Extracting file {num + 1}/{count}...");
							}
						}
					}
					_logger.LogInfo((object)("Finished processing zip file '" + relativePath + "'."));
				}
				catch (Exception ex)
				{
					HandleException(ex);
				}
			};
		}

		private void RunActions()
		{
			if (_currentActions.Count == 0)
			{
				OnComplete();
				return;
			}
			Task.Run(delegate
			{
				foreach (Action currentAction in _currentActions)
				{
					currentAction();
				}
				OnComplete();
			});
		}

		private void Clear()
		{
			_currentActions.Clear();
		}

		private void HandleException(Exception ex)
		{
			if (ex is IOException || ex is UnauthorizedAccessException || ex is SecurityException || ex is InvalidDataException || ex is FileNotFoundException)
			{
				_logger.LogError((object)ex);
				_hasErrors = true;
				return;
			}
			throw ex;
		}

		private void CheckForErrors()
		{
			if (_hasErrors)
			{
				_logger.LogInfo((object)"Map installer encountered errors trying to copy maps, for support please visit the Cities2Modding discord referencing the error.");
				_logger.LogInfo((object)"See BepInEx log file at: 'BepInEx\\plugins' folder.");
			}
		}

		private void OnComplete()
		{
			CheckForErrors();
			Clear();
		}

		public void Run()
		{
			EnsureMapsFolder();
			ScanDirectory();
			RunActions();
		}
	}
	[BepInPlugin("MapInstaller", "MapInstaller", "1.0.0")]
	public class Plugin : BaseUnityPlugin
	{
		private void Awake()
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)"=================================================================");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"MapInstaller by Cities2Modding community.");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"=================================================================");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Reddit link: https://www.reddit.com/r/cities2modding/");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Discord link: https://discord.gg/KGRNBbm5Fh");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Our mods are officially distributed via Thunderstore.io and https://github.com/Cities2Modding");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Example mod repository and modding info: https://github.com/optimus-code/Cities2Modding");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Thanks to Captain_Of_Coit, 89pleasure, Rebecca, optimus-code and the Cites2Modding community!");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"=================================================================");
			new Installer(((BaseUnityPlugin)this).Logger).Run();
		}
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "MapInstaller";

		public const string PLUGIN_NAME = "MapInstaller";

		public const string PLUGIN_VERSION = "1.0.0";
	}
}