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";
}
}