Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions NetworkExtensions2/Compatibility/CitiesHarmony/HarmonyHelper.cs
Original file line number Diff line number Diff line change
@@ -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<Action> _harmonyReadyActions = new List<Action>();

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<Assembly, string>)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;
}
}
}
}
234 changes: 234 additions & 0 deletions NetworkExtensions2/Compatibility/CitiesHarmony/SubscriptionPrompt.cs
Original file line number Diff line number Diff line change
@@ -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>("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>("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>("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;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CitiesHarmony.API;
using CitiesHarmony.API;
using ICities;
using NetworkExtensions2.Patching;
using System.Diagnostics;
Expand Down
Loading