diff --git a/NetworkExtensions2/Compatibility/CitiesHarmony/HarmonyHelper.cs b/NetworkExtensions2/Compatibility/CitiesHarmony/HarmonyHelper.cs new file mode 100644 index 00000000..4ec8c19c --- /dev/null +++ b/NetworkExtensions2/Compatibility/CitiesHarmony/HarmonyHelper.cs @@ -0,0 +1,152 @@ +using ColossalFramework.PlatformServices; +using ColossalFramework.Plugins; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace CitiesHarmony.API +{ + public static class HarmonyHelper + { + public static Version MinHarmonyVersion => new Version(2, 2, 2, 0); + + internal const ulong CitiesHarmonyWorkshopId = 2040656402uL; + + private static bool _workshopItemInstalledSubscribed = false; + private static List _harmonyReadyActions = new List(); + + public static bool IsHarmonyInstalled => InvokeHarmonyInstaller(); + + public static void EnsureHarmonyInstalled() + { + if (!IsHarmonyInstalled) + { + SubscriptionPrompt.ShowOnce(); + } + } + + public static void DoOnHarmonyReady(Action action) + { + if (IsHarmonyInstalled) + { + action(); + } + else + { + _harmonyReadyActions.Add(action); + + if (!_workshopItemInstalledSubscribed && SteamWorkshopAvailable) + { + _workshopItemInstalledSubscribed = true; + PlatformService.workshop.eventWorkshopItemInstalled += OnWorkshopItemInstalled; + } + + SubscriptionPrompt.ShowOnce(); + } + } + + private static bool InvokeHarmonyInstaller() + { + var installerRunMethod = GetInstallerRunMethod(); + if (installerRunMethod == null) + return false; + + installerRunMethod.Invoke(null, new object[0]); + + if (!IsCurrentHarmonyVersionLoaded) + return false; + + return true; + } + + internal static bool IsInstallerLoaded => GetInstallerRunMethod() != null; + + private static MethodInfo GetInstallerRunMethod() + { + return Type.GetType("CitiesHarmony.Installer, CitiesHarmony", false)?.GetMethod("Run", BindingFlags.Public | BindingFlags.Static); + } + + internal static bool IsCurrentHarmonyVersionLoaded => GetLoadedHarmonyVersion() >= MinHarmonyVersion; + + internal static Version GetLoadedHarmonyVersion() + { + try + { + // we are using this dict from PluginManager to get the assembly locations + // (assembly.Location and assembly.CodeBase return empty/incorrect paths) + var assemblyLocationsField = typeof(PluginManager).GetField("m_AssemblyLocations", BindingFlags.NonPublic | BindingFlags.Instance); + var assemblyLocations = (Dictionary)assemblyLocationsField.GetValue(PluginManager.instance); + Version result = default; + foreach (var pair in assemblyLocations) + { + var assemblyName = pair.Key.GetName(); + if ((assemblyName.Name == "CitiesHarmony.Harmony") && assemblyName.Version.Major >= 2) + { + // we are using the file version to determine the minor version + // because increasing the assembly version breaks the game's assembly resolution + // (we are stuck at assembly version 2.0.4.0 forever) + var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(pair.Value); + var fileVersion = new Version(fvi.FileVersion); + if (result == default || fileVersion < result) + result = fileVersion; + } + } + return result; + } + catch (Exception e) + { + UnityEngine.Debug.LogException(e); + return MinHarmonyVersion; // safety in case future game code changes are breaking this + } + } + + + private static bool SteamWorkshopAvailable => PlatformService.platformType == PlatformType.Steam && !PluginManager.noWorkshop; + + private static void OnWorkshopItemInstalled(PublishedFileId id) + { + if (id.AsUInt64 == CitiesHarmonyWorkshopId) + { + UnityEngine.Debug.Log("CitiesHarmony workshop item subscribed and loaded!"); + + if (InvokeHarmonyInstaller()) + { + foreach (var action in _harmonyReadyActions) RunHarmonyReadyAction(action); + _harmonyReadyActions.Clear(); + } + else + { + UnityEngine.Debug.LogError("Failed to invoke Harmony installer!"); + } + } + } + + private static void RunHarmonyReadyAction(Action action) + { + try + { + action(); + } + catch (Exception e) + { + UnityEngine.Debug.LogException(e); + } + } + + internal static bool IsCitiesHarmonyWorkshopItemSubscribed + { + get + { + var subscribedIds = PlatformService.workshop.GetSubscribedItems(); + if (subscribedIds == null) return false; + + foreach (var id in subscribedIds) + { + if (id.AsUInt64 == CitiesHarmonyWorkshopId) return true; + } + + return false; + } + } + } +} diff --git a/NetworkExtensions2/Compatibility/CitiesHarmony/SubscriptionPrompt.cs b/NetworkExtensions2/Compatibility/CitiesHarmony/SubscriptionPrompt.cs new file mode 100644 index 00000000..9c05c3bd --- /dev/null +++ b/NetworkExtensions2/Compatibility/CitiesHarmony/SubscriptionPrompt.cs @@ -0,0 +1,234 @@ +using ColossalFramework.PlatformServices; +using ColossalFramework.Plugins; +using ColossalFramework.UI; +using ICities; +using System; +using System.Reflection; +using System.Text; + +namespace CitiesHarmony.API +{ + public static class SubscriptionPrompt + { + private const string Marker = "Harmony2SubscriptionWarning"; + + public static void ShowOnce() + { + if (UnityEngine.GameObject.Find(Marker)) return; + + var go = new UnityEngine.GameObject(Marker); + UnityEngine.Object.DontDestroyOnLoad(go); + + if (LoadingManager.instance.m_currentlyLoading || UIView.library == null) + { + LoadingManager.instance.m_introLoaded += OnIntroLoaded; + LoadingManager.instance.m_levelLoaded += OnLevelLoaded; + } + else + { + Show(); + } + } + + private static void OnIntroLoaded() + { + LoadingManager.instance.m_introLoaded -= OnIntroLoaded; + LoadingManager.instance.m_levelLoaded -= OnLevelLoaded; + Show(); + } + + private static void OnLevelLoaded(SimulationManager.UpdateMode updateMode) + { + LoadingManager.instance.m_introLoaded -= OnIntroLoaded; + LoadingManager.instance.m_levelLoaded -= OnLevelLoaded; + Show(); + } + + private static void Show() + { + if (!HarmonyHelper.IsInstallerLoaded) + { + ShowSubscriptionPrompt(); + } + else if (!HarmonyHelper.IsCurrentHarmonyVersionLoaded) + { + ShowOutdatedPrompt(); + } + } + + private static void ShowSubscriptionPrompt() + { + if (!GetSubscriptionHelpMessages(out var reason, out var solution)) + { + ShowError(reason, solution); + return; + } + else + { + ConfirmPanel.ShowModal("Missing dependency: Harmony", + "The dependency 'Harmony' is required for some mod(s) to work correctly. Do you want to subscribe to it in the Steam Workshop?", + OnConfirm); + } + } + + private static void OnConfirm(UIComponent component, int result) + { + if (result == 1) + { + UnityEngine.Debug.Log("Subscribing to CitiesHarmony workshop item!"); + + if (PlatformService.workshop.Subscribe(new PublishedFileId(HarmonyHelper.CitiesHarmonyWorkshopId))) + { + UIView.library.ShowModal("ExceptionPanel").SetMessage("Success!", + "Harmony has been installed successfully. It is recommended to restart the game now!", false); + } + else + { + ShowError("An error occured while attempting to automatically subscribe to Harmony (no network connection?)", + "You can manually download the Harmony mod from github.com/boformer/CitiesHarmony/releases"); + } + } + else + { + ShowError("You have rejected to automatically subscribe to Harmony :(", + "Either unsubscribe those mods or subscribe to the Harmony mod, then restart the game!"); + } + } + + private static void ShowError(string reason, string solution) + { + var affectedAssemblyNames = new StringBuilder(); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + if (RequiresHarmony2(assembly)) + { + affectedAssemblyNames.Append("• ").Append(GetModName(assembly)).Append('\n'); + } + } + + var message = $"The mod(s):\n{affectedAssemblyNames}require the dependency 'Harmony' to work correctly!\n\n{reason}\n\nClose the game, {solution}"; + + UIView.library.ShowModal("ExceptionPanel").SetMessage("Missing dependency: Harmony", message, false); + } + + private static void ShowOutdatedPrompt() + { + var loadedVersion = HarmonyHelper.GetLoadedHarmonyVersion(); + + var affectedAssemblyNames = new StringBuilder(); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + if (RequiresHarmony2(assembly)) + { + Version requiredVersion = GetRequiredHarmonyVersion(assembly); + if (requiredVersion > loadedVersion) + { + affectedAssemblyNames.Append("• ").Append(GetModName(assembly)).Append(" (requires ").Append(requiredVersion).Append(")\n"); + } + + } + } + + GetSubscriptionHelpMessages(out _, out var solution); + var message = $"The mod(s):\n{affectedAssemblyNames}require a newer version of the dependency 'Harmony' to work correctly!\n\nClose the game, {solution}"; + + UIView.library.ShowModal("ExceptionPanel").SetMessage($"Outdated Harmony version {loadedVersion}", message, false); + } + + private static bool GetSubscriptionHelpMessages(out string reason, out string solution) + { + if (PlatformService.platformType != PlatformType.Steam) + { + UnityEngine.Debug.LogError("Cannot auto-subscribe CitiesHarmony on platforms other than Steam!"); + reason = "Harmony could not be installed automatically because you are using a platform other than Steam."; + solution = "then manually download and install the Harmony mod from github.com/boformer/CitiesHarmony/releases"; + return false; + } + + if (PluginManager.noWorkshop) + { + UnityEngine.Debug.LogError("Cannot auto-subscribe CitiesHarmony in --noWorkshop mode!"); + reason = "Harmony could not be installed automatically because you are playing in --noWorkshop mode!"; + solution = "then restart without --noWorkshop or manually download and install the Harmony mod from github.com/boformer/CitiesHarmony/releases"; + return false; + } + + if (!PlatformService.workshop.IsAvailable()) + { + UnityEngine.Debug.LogError("Cannot auto-subscribe CitiesHarmony while workshop is not available"); + reason = "Harmony could not be installed automatically because the Steam workshop is not available (no network connection?)"; + solution = "then manually download and install the Harmony mod from github.com/boformer/CitiesHarmony/releases"; + return false; + } + + + if (HarmonyHelper.IsCitiesHarmonyWorkshopItemSubscribed) + { + UnityEngine.Debug.LogError("CitiesHarmony workshop item is subscribed, but assembly is not loaded or outdated!"); + reason = "It seems that Harmony has already been subscribed, but Steam failed to download the files correctly or they were deleted."; + solution = "uninstall all local or alternative versions of the Harmony mod, then (re)subscribe to the Harmony workshop item from steamcommunity.com/sharedfiles/filedetails/?id=2040656402"; + return false; + } + + + reason = ""; + solution = "uninstall all local or alternative versions of the Harmony mod, then (re)subscribe to the Harmony workshop item from steamcommunity.com/sharedfiles/filedetails/?id=2040656402"; + return true; + } + + private static bool RequiresHarmony2(Assembly assembly) + { + if (assembly.GetName().Name == "0Harmony" || assembly.GetName().Name == "CitiesHarmony.Harmony") return false; + + foreach (var assemblyName in assembly.GetReferencedAssemblies()) + { + if ((assemblyName.Name == "0Harmony" || assemblyName.Name == "CitiesHarmony.Harmony") && assemblyName.Version.Major >= 2) + { + return true; + } + } + return false; + } + + private static Version GetRequiredHarmonyVersion(Assembly assembly) + { + foreach (var assemblyName in assembly.GetReferencedAssemblies()) + { + if (assemblyName.Name == "CitiesHarmony.API") + { + try + { + var versionGetter = Assembly.Load(assemblyName) + ?.GetType("CitiesHarmony.API.HarmonyHelper") + ?.GetProperty("MinHarmonyVersion", BindingFlags.Static | BindingFlags.Public) + ?.GetGetMethod(); + + if (versionGetter != null && versionGetter.ReturnType == typeof(Version)) + { + return (Version)versionGetter.Invoke(null, new object[0]); + } + } + catch (Exception e) + { + UnityEngine.Debug.LogException(e); + } + } + } + + return new Version(2, 0, 4, 0); // fallback: last version before we added the FileVersion logic + } + + private static string GetModName(Assembly assembly) + { + foreach (var plugin in PluginManager.instance.GetPluginsInfo()) + { + if (plugin.userModInstance is IUserMod mod && mod.GetType().Assembly == assembly) + return mod.Name; + } + + return assembly.GetName().Name; + } + } +} diff --git a/NetworkExtensions2/Framework/Mod/TransitModBase.LoadingExtension.cs b/NetworkExtensions2/Framework/Mod/TransitModBase.LoadingExtension.cs index fdadb523..1349c3ae 100644 --- a/NetworkExtensions2/Framework/Mod/TransitModBase.LoadingExtension.cs +++ b/NetworkExtensions2/Framework/Mod/TransitModBase.LoadingExtension.cs @@ -1,4 +1,4 @@ -using CitiesHarmony.API; +using CitiesHarmony.API; using ICities; using NetworkExtensions2.Patching; using System.Diagnostics; diff --git a/NetworkExtensions2/Framework/Mod/TransitModBase.cs b/NetworkExtensions2/Framework/Mod/TransitModBase.cs index b9115d67..49def6e3 100644 --- a/NetworkExtensions2/Framework/Mod/TransitModBase.cs +++ b/NetworkExtensions2/Framework/Mod/TransitModBase.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using ColossalFramework.IO; using ColossalFramework.PlatformServices; using ICities; @@ -37,17 +37,8 @@ public virtual string AssetPath { if (_assetPath == null) { - var publishedFileID = PluginInfo.publishedFileID.AsUInt64; - _assetPath = GetAssetPath(DefaultFolderPath, publishedFileID); - - if (_assetPath != Assets.PATH_NOT_FOUND) - { - Debug.Log("TFW: Mod path " + _assetPath); - } - else - { - Debug.Log("TFW: Path not found"); - } + _assetPath = PluginInfo.modPath; + Debug.Log("TFW: Mod path " + _assetPath); } return _assetPath; } diff --git a/NetworkExtensions2/Menus/Roads/RExExtendedMenus.cs b/NetworkExtensions2/Menus/Roads/RExExtendedMenus.cs index c0fe1a4d..99c3e156 100644 --- a/NetworkExtensions2/Menus/Roads/RExExtendedMenus.cs +++ b/NetworkExtensions2/Menus/Roads/RExExtendedMenus.cs @@ -1,4 +1,4 @@ - + namespace Transit.Addon.RoadExtensions.Menus.Roads { public static class RExExtendedMenus @@ -7,5 +7,10 @@ public static class RExExtendedMenus public const string ROADS_SMALL_HV = "RoadsSmallHV"; public const string ROADS_BUSWAYS = "RoadsBusways"; public const string ROADS_PEDESTRIANS = "RoadsPedestrians"; + + // Modern DLC Categories + public const string ROADS_TRAMS = "RoadsTrams"; + public const string ROADS_MONORAIL = "RoadsMonorail"; + public const string ROADS_TROLLEYBUS = "RoadsTrolleybus"; } } diff --git a/NetworkExtensions2/Mod.cs b/NetworkExtensions2/Mod.cs index 16df3731..d9199215 100644 --- a/NetworkExtensions2/Mod.cs +++ b/NetworkExtensions2/Mod.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using ColossalFramework; using ColossalFramework.Plugins; @@ -21,7 +21,7 @@ public override string Description public override string Version { - get { return "1.0.0"; } + get { return "2.1.0"; } } private const string NEXT_2_ID = "812125426"; diff --git a/NetworkExtensions2/NetworkExtensions2.csproj b/NetworkExtensions2/NetworkExtensions2.csproj index 0a333c17..500cf9aa 100644 --- a/NetworkExtensions2/NetworkExtensions2.csproj +++ b/NetworkExtensions2/NetworkExtensions2.csproj @@ -1,4 +1,4 @@ - + Debug @@ -40,13 +40,6 @@ ..\References\Assembly-CSharp.dll False - - ..\packages\CitiesHarmony.API.2.1.0\lib\net35\CitiesHarmony.API.dll - - - ..\packages\CitiesHarmony.Harmony.2.2.0\lib\net35\CitiesHarmony.Harmony.dll - False - ..\References\ColossalManaged.dll False @@ -55,6 +48,10 @@ ..\References\ICities.dll False + + ..\References\CitiesHarmony.Harmony.dll + False + ..\References\ObjUnity3D.dll @@ -62,12 +59,17 @@ - D:\Program Files\SteamLibrary\steamapps\common\Cities_Skylines\Cities_Data\Managed\UnityEngine.dll + ..\References\UnityEngine.dll False + + + + + @@ -236,7 +238,7 @@ - + @@ -284,7 +286,7 @@ - + @@ -3289,37 +3291,37 @@ Always - + Always - + Always - + Always - + Always - + Always - + Always - + Always - + Always - + Always - + Always - + Always @@ -4244,20 +4246,20 @@ - + forfiles /P "$(ProjectDir)bin\$(ConfigurationName)" /S /M *.CRP /C "cmd /c move @file "$(ProjectDir)bin\$(ConfigurationName)"" if $(ConfigurationName) == Debug ( -rd /s /q "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" -mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" -xcopy /q /y /e "$(ProjectDir)bin\$(ConfigurationName)" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" +rd /s /q "%LOCALAPPDATA%\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" +mkdir "%LOCALAPPDATA%\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" +xcopy /q /y /e "$(ProjectDir)bin\$(ConfigurationName)" "%LOCALAPPDATA%\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" ) else ( -rd /s /q "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" -mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" -xcopy /q /y /e "$(ProjectDir)bin\$(ConfigurationName)" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" +rd /s /q "%LOCALAPPDATA%\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" +mkdir "%LOCALAPPDATA%\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" +xcopy /q /y /e "$(ProjectDir)bin\$(ConfigurationName)" "%LOCALAPPDATA%\Colossal Order\Cities_Skylines\Addons\Mods\NetworkExtensions2" ) - + del /f /q "$(TargetDir)"