diff --git a/TLM/SharedAssemblyInfo.cs b/TLM/SharedAssemblyInfo.cs
index c92c8cb60..db08c260c 100644
--- a/TLM/SharedAssemblyInfo.cs
+++ b/TLM/SharedAssemblyInfo.cs
@@ -1,11 +1,10 @@
using System.Reflection;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
-[assembly: AssemblyCompany("")]
+[assembly: AssemblyCompany("https://TMPE.me")]
[assembly: AssemblyCopyright("Copyright © 2020")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
diff --git a/TLM/TLM/Compatibility/Check/Assemblies.cs b/TLM/TLM/Compatibility/Check/Assemblies.cs
new file mode 100644
index 000000000..ce385b5b7
--- /dev/null
+++ b/TLM/TLM/Compatibility/Check/Assemblies.cs
@@ -0,0 +1,110 @@
+namespace TrafficManager.Compatibility.Check {
+ using ColossalFramework;
+ using ColossalFramework.Plugins;
+ using CSUtil.Commons;
+ using ICities;
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Reflection;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using static ColossalFramework.Plugins.PluginManager;
+ using TrafficManager.Util;
+
+ public class Assemblies {
+
+ ///
+ /// Default build string if we can't determine LABS, STABLE, or DEBUG.
+ ///
+ internal const string OBSOLETE = "OBSOLETE";
+
+ internal const string STABLE = "STABLE";
+
+ internal const string BROKEN = "BROKEN";
+
+ internal const string TMMOD = "TrafficManager.TrafficManagerMod";
+
+ internal static readonly Version VersionedByAssembly;
+
+ internal static readonly Version LinuxFanVersion;
+
+ ///
+ /// Initializes static members of the class.
+ ///
+ static Assemblies() {
+ VersionedByAssembly = new Version(11, 1, 0);
+ LinuxFanVersion = new Version(10, 20);
+ }
+
+ public static bool Verify(/*out Dictionary results*/) {
+
+ Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ foreach (Assembly asm in assemblies) {
+ AssemblyName details = asm.GetName();
+ if (details.Name.Contains("TrafficManager")) {
+
+ try {
+ //Log.Info("--------------------------- extracting ver info ---------------");
+
+ if (ExtractVersionDetails(asm, out Version ver, out string build)) {
+ Log.InfoFormat(
+ "Assembly: {0} v{1} {2}",
+ details.Name,
+ ver.Build == -1 ? ver.ToString(2) : ver.ToString(3),
+ build);
+ }
+
+ } catch (Exception e) {
+ Log.Info("loop failed -----------");
+ Log.Error(e.ToString());
+ }
+ }
+ }
+ return true; // to do
+ }
+
+ internal static bool ExtractVersionDetails(Assembly asm, out Version ver, out string branch) {
+
+ ver = asm.GetName().Version;
+ branch = OBSOLETE;
+
+ Type type = asm.GetType(TMMOD);
+ object instance = Activator.CreateInstance(type);
+
+ if (ver < VersionedByAssembly) {
+ try {
+ if (MemberValue.TryGetMemberValue(type, instance, "Version", out string dirty)) {
+ //Log.Info("Raw string: " + dirty);
+
+ // clean the raw string in to something that resembles a verison number
+ string clean = Regex.Match(dirty, @"[0-9]+(?:\.[0-9]+)+").Value;
+ //Log.Info("clean string: " + clean);
+
+ // parse in to Version instance
+ ver = new Version(clean);
+ }
+ }
+ catch {
+ Log.Warning("Unable to retrieve or parse 'Version' member");
+ }
+ }
+
+ try {
+ if (MemberValue.TryGetMemberValue(type, instance, "BRANCH", out string val)) {
+ branch = val;
+ } else if (ver == LinuxFanVersion) { // temporary
+ branch = STABLE;
+ }
+ }
+ catch {
+ Log.Warning("Unable to retrieve or parse 'BRANCH' member");
+ }
+
+ (instance as IDisposable)?.Dispose();
+
+ return true;
+ }
+
+ }
+}
diff --git a/TLM/TLM/Compatibility/Check/DLCs.cs b/TLM/TLM/Compatibility/Check/DLCs.cs
new file mode 100644
index 000000000..8cfb7799e
--- /dev/null
+++ b/TLM/TLM/Compatibility/Check/DLCs.cs
@@ -0,0 +1,53 @@
+namespace TrafficManager.Compatibility.Check {
+ using ColossalFramework.PlatformServices;
+ using CSUtil.Commons;
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ ///
+ /// Scans for transit-affecting DLCs.
+ ///
+ public class DLCs {
+
+ // Strings for log entries
+ internal const string LOG_ENTRY_FORMAT = " {0} {1}\n";
+ internal const string MARKER_ENABLED = "*";
+ internal const string MARKER_BLANK = " ";
+
+ ///
+ /// Scan for DLCs and log whether they are installed or not.
+ ///
+ public static void Verify() {
+
+ try {
+ Dictionary DLCs = new Dictionary() {
+ { (uint)SteamHelper.DLC.AfterDarkDLC, "After Dark" },
+ { (uint)SteamHelper.DLC.InMotionDLC, "Mass Transit" },
+ { (uint)SteamHelper.DLC.SnowFallDLC, "Snowfall" },
+ { (uint)SteamHelper.DLC.NaturalDisastersDLC, "Natural Disasters" },
+ { (uint)SteamHelper.DLC.ParksDLC, "Park Life" },
+ { (uint)SteamHelper.DLC.IndustryDLC, "Industries" },
+ { (uint)SteamHelper.DLC.GreenCitiesDLC, "Green Cities" },
+ { (uint)SteamHelper.DLC.Football, "Match Day" },
+ };
+
+ StringBuilder sb = new StringBuilder(500);
+
+ sb.Append("Transit-affecting DLCs [*] = Installed:\n\n");
+
+ foreach (KeyValuePair dlc in DLCs) {
+ sb.AppendFormat(
+ LOG_ENTRY_FORMAT,
+ PlatformService.IsDlcInstalled(dlc.Key) ? MARKER_ENABLED : MARKER_BLANK,
+ dlc.Value);
+ }
+
+ Log.Info(sb.ToString());
+ }
+ catch (Exception e) {
+ Log.ErrorFormat("Error logging DLC\n{0}", e.ToString());
+ }
+ }
+ }
+}
diff --git a/TLM/TLM/Compatibility/Check/Mods.cs b/TLM/TLM/Compatibility/Check/Mods.cs
new file mode 100644
index 000000000..332344b99
--- /dev/null
+++ b/TLM/TLM/Compatibility/Check/Mods.cs
@@ -0,0 +1,166 @@
+namespace TrafficManager.Compatibility.Check {
+ using ColossalFramework.Plugins;
+ using ColossalFramework;
+ using CSUtil.Commons;
+ using static ColossalFramework.Plugins.PluginManager;
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+ using TrafficManager.Compatibility.Enum;
+ using TrafficManager.Compatibility.Struct;
+ using TrafficManager.State;
+
+ ///
+ /// Scans for known incompatible mods as defined by .
+ ///
+ public class Mods {
+
+ // Strings for log entries
+ internal const string LOG_ENTRY_FORMAT = "{0} {1} {2} {3}\n";
+ internal const string MARKER_ENABLED = "*";
+ internal const string MARKER_BLANK = " ";
+ internal const string MARKER_TMPE = ">";
+ internal const string MARKER_CRITICAL = "C";
+ internal const string MARKER_MAJOR = "M";
+ internal const string MARKER_MINOR = "m";
+
+ internal const string LOCAL_MOD_STR = "(local)";
+ internal const string BUNDLED_MOD_STR = "(bundled)";
+
+ ///
+ /// Scans installed mods (local and workshop) looking for known incompatibilities.
+ ///
+ ///
+ /// A dictionary issues found (will be empty if no issues).
+ /// Number of minor incompatibilities.
+ /// Number of major incompatibilities.
+ /// Number of critical incompatibilities.
+ /// Number of non-obsolete TM:PE mods.
+ ///
+ /// Returns true if incompatible mods detected, otherwise false.
+ public static bool Verify(
+ out Dictionary results,
+ out int minor,
+ out int major,
+ out int critical,
+ out int tmpe) {
+
+ results = new Dictionary();
+
+ // current verification state
+ bool verified = true;
+
+ // check minor severity incompatibilities?
+ bool scanMinor = GlobalConfig.Instance.Main.ScanForKnownIncompatibleModsAtStartup;
+
+ // check disabled mods? note: Critical incompatibilities are always processed
+ bool scanDisabled = !GlobalConfig.Instance.Main.IgnoreDisabledMods;
+
+ // batch all logging in to a single log message
+ // 6000 chars is roughly 120 mods worth of logging
+ StringBuilder log = new StringBuilder(6000);
+
+ log.AppendFormat(
+ "Compatibility.Check.Mods.Verify() scanMinor={0}, scanDisabled={1}\n\n",
+ scanMinor,
+ scanDisabled);
+
+ // Variables for log file entries
+ string logWorkshopId;
+ string logIncompatible;
+
+ // problem counters
+ minor = major = critical = tmpe = 0;
+
+ PluginManager manager = Singleton.instance;
+
+ List mods = new List(manager.modCount);
+
+ mods.AddRange(manager.GetPluginsInfo()); // normal mods
+ mods.AddRange(manager.GetCameraPluginInfos()); // camera scripts
+
+ // iterate plugins
+ foreach (PluginInfo mod in mods) {
+
+ try {
+ // Generate descriptor for the mod
+ ModDescriptor descriptor = mod;
+
+ results.Add(mod, descriptor);
+
+ // String to log for workshop id
+ logWorkshopId = mod.isBuiltin
+ ? BUNDLED_MOD_STR
+ : descriptor.ModIsLocal
+ ? LOCAL_MOD_STR
+ : descriptor.ModWorkshopId.ToString();
+
+ switch (descriptor.Incompatibility) {
+
+ case Severity.Critical:
+ logIncompatible = MARKER_CRITICAL;
+ ++critical;
+ verified = false;
+ break;
+
+ case Severity.Major:
+ logIncompatible = MARKER_MAJOR;
+ ++major;
+ if (mod.isEnabled || scanDisabled) {
+ verified = false;
+ }
+ break;
+
+ case Severity.Minor:
+ logIncompatible = MARKER_MINOR;
+ ++minor;
+ if (scanMinor && (mod.isEnabled || scanDisabled)) {
+ verified = false;
+ }
+ break;
+
+ case Severity.TMPE:
+ logIncompatible = MARKER_TMPE;
+ ++tmpe;
+ if (descriptor.AssemblyGuid != CompatibilityManager.SelfGuid) {
+ verified = false;
+ }
+ break;
+
+ default:
+ case Severity.None:
+ logIncompatible = MARKER_BLANK;
+ break;
+ }
+
+ log.AppendFormat(
+ LOG_ENTRY_FORMAT,
+ logIncompatible,
+ mod.isEnabled ? MARKER_ENABLED : MARKER_BLANK,
+ logWorkshopId.PadRight(12),
+ descriptor.ModName);
+
+ } catch (Exception e) {
+ Log.ErrorFormat(
+ "Error scanning {0}:\n{1}",
+ mod.modPath,
+ e.ToString());
+ }
+
+ } // foreach
+
+ log.AppendFormat(
+ "\n{0} Mod(s): {1} [*] enabled, {2} [C]ritical, {3} [M]ajor, {4} [m]inor, {5} [>] TM:PE\n",
+ manager.modCount,
+ manager.enabledModCount,
+ critical,
+ major,
+ minor,
+ tmpe);
+
+ Log.Info(log.ToString());
+
+ return verified;
+ }
+ }
+}
diff --git a/TLM/TLM/Compatibility/Check/Versions.cs b/TLM/TLM/Compatibility/Check/Versions.cs
new file mode 100644
index 000000000..b59f44ca5
--- /dev/null
+++ b/TLM/TLM/Compatibility/Check/Versions.cs
@@ -0,0 +1,41 @@
+namespace TrafficManager.Compatibility.Check {
+ using CSUtil.Commons;
+ using System;
+
+ ///
+ /// Checks version equality with added logging.
+ ///
+ public class Versions {
+
+ ///
+ /// Verifies that expected version matches actual verison.
+ ///
+ /// Versions are matched on Major.minor.build. Revision is ignored.
+ ///
+ ///
+ /// The version you are expecting.
+ /// The actual version.
+ ///
+ /// Returns true if versions match, otherwise false.
+ public static bool Verify(Version expected, Version actual) {
+ Log.InfoFormat(
+ "Compatibility.Check.Versions.Verify({0}, {1})",
+ expected.ToString(3),
+ actual.ToString(3));
+
+ return expected == actual;
+ }
+
+ ///
+ /// Returns the game version as a instance.
+ ///
+ ///
+ /// Game version.
+ public static Version GetGameVersion() {
+ return new Version(
+ Convert.ToInt32(BuildConfig.APPLICATION_VERSION_A),
+ Convert.ToInt32(BuildConfig.APPLICATION_VERSION_B),
+ Convert.ToInt32(BuildConfig.APPLICATION_VERSION_C));
+ }
+ }
+}
diff --git a/TLM/TLM/Compatibility/CompatibilityManager.cs b/TLM/TLM/Compatibility/CompatibilityManager.cs
new file mode 100644
index 000000000..7b1b6c1c9
--- /dev/null
+++ b/TLM/TLM/Compatibility/CompatibilityManager.cs
@@ -0,0 +1,279 @@
+namespace TrafficManager.Compatibility {
+ using ColossalFramework;
+ using ColossalFramework.Plugins;
+ using static ColossalFramework.Plugins.PluginManager;
+ using ColossalFramework.UI;
+ using CSUtil.Commons;
+ using System;
+ using System.Collections.Generic;
+ using System.Reflection;
+ using TrafficManager.Compatibility.Struct;
+ using UnityEngine;
+ using UnityEngine.SceneManagement;
+
+ ///
+ /// Manages pre-flight checks for known incompatible mods.
+ ///
+ public class CompatibilityManager {
+
+ ///
+ /// The Guid of the executing assembly (used to filter self from incompatibility checks).
+ ///
+ public static readonly Guid SelfGuid;
+
+ ///
+ /// When true, don't perform checks or show UI.
+ ///
+ private static bool paused_ = true;
+
+ ///
+ /// When true, a game restart is required.
+ ///
+ private static bool restartRequired_ = false;
+
+ ///
+ /// When true, user wants to auto-load most recent save.
+ ///
+ private static bool autoContinue_ = false;
+
+ ///
+ /// Initializes static members of the class.
+ ///
+ static CompatibilityManager() {
+ StopLauncherAutoContinue();
+
+ SelfGuid = Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId;
+ }
+
+ ///
+ /// Run checks when possible to do so.
+ ///
+ public static void Activate() {
+ Log._DebugFormat(
+ "CompatibilityManager.Activate() Scene = {0}",
+ SceneManager.GetActiveScene().name);
+
+ // Abort if this is an in-game hot reload
+ if (SceneManager.GetActiveScene().name == "Game") {
+ Log._Debug("- Skipping due to in-game hot reload");
+ paused_ = true;
+ return;
+ }
+
+ paused_ = UIView.GetAView() == null;
+
+ if (paused_) {
+ Log._Debug("- Waiting for main menu...");
+ LoadingManager.instance.m_introLoaded += OnIntroLoaded;
+ } else {
+ PerformChecks();
+ SetEvents(true);
+ }
+ }
+
+ ///
+ /// Deactivates the compatibility checker.
+ ///
+ public static void Deactivate() {
+ Log._Debug("CompatibilityManager.Deactivate()");
+
+ paused_ = true;
+
+ // todo: destroy IncompatibleMods.Instance.List
+
+ SetEvents(false);
+ }
+
+ ///
+ /// Removes all event listeners and, optionally, add all event listeners.
+ ///
+ ///
+ /// If true then event listeners are added.
+ private static void SetEvents(bool active) {
+
+ SceneManager.activeSceneChanged -= OnSceneChanged;
+ LoadingManager.instance.m_introLoaded -= OnIntroLoaded;
+ Singleton.instance.eventPluginsChanged -= OnPluginsChanged;
+ Singleton.instance.eventPluginsStateChanged -= OnPluginsChanged;
+
+ if (active) {
+ SceneManager.activeSceneChanged += OnSceneChanged;
+ Singleton.instance.eventPluginsChanged += OnPluginsChanged;
+ Singleton.instance.eventPluginsStateChanged += OnPluginsChanged;
+ }
+ }
+
+ ///
+ /// Checks and logs:
+ ///
+ /// * Game version
+ /// * Incompatible mods
+ /// * Multiple TM:PE versions
+ /// * Zombie assemblies
+ /// * Traffic-affecting DLCs.
+ ///
+ private static void PerformChecks() {
+ Log.InfoFormat(
+ "CompatibilityManager.PerformChecks() GUID = {0}",
+ SelfGuid);
+
+ if (!Check.Versions.Verify(
+ TrafficManagerMod.ExpectedGameVersion,
+ Check.Versions.GetGameVersion())) {
+
+ autoContinue_ = false;
+
+ //todo: show warning about game version
+ }
+
+ if (!Check.Mods.Verify(
+ out Dictionary results,
+ out int minor,
+ out int major,
+ out int critical,
+ out int candidate)) {
+
+ restartRequired_ = true;
+
+ // todo: deal with incompatibilities
+ }
+
+ // If a restart is not yet required, check for zombie assemblies
+ // which are the main cause of save/load issues.
+ if (!restartRequired_ && !Check.Assemblies.Verify()) {
+
+ restartRequired_ = true;
+
+ // todo: show warning about settings loss
+ }
+
+ Check.DLCs.Verify();
+
+ if (restartRequired_) {
+ autoContinue_ = false;
+ } else {
+ ResumeLauncherAutoContinue();
+ }
+ }
+
+ ///
+ /// Triggered when app intro screens have finished.
+ ///
+ private static void OnIntroLoaded() {
+ Log._Debug("CompatibilityManager.OnIntroLoaded()");
+
+ paused_ = false;
+ PerformChecks();
+ SetEvents(true);
+ }
+
+ ///
+ /// Triggered by plugin subscription/state change.
+ ///
+ private static void OnPluginsChanged() {
+ Log._Debug("CompatibilityManager.OnPluginsChanged()");
+
+ if (!paused_) {
+ PerformChecks();
+ }
+ }
+
+ ///
+ /// Triggered by scene changes.
+ ///
+ ///
+ /// The current (usually empty).
+ /// The being transitioned to.
+ private static void OnSceneChanged(Scene current, Scene next) {
+ Log._DebugFormat(
+ "CompatibilityManager.OnSceneChanged('{0}','{1}')",
+ current.name,
+ next.name);
+
+ paused_ = next.name != "MainMenu";
+ }
+
+ /*
+ private static bool CanWeDoStuff() {
+ if (SceneManager.GetActiveScene().name == "MainMenu") {
+ return true;
+ }
+
+ // make sure we're not loading a game/asset/etc
+ if (Singleton.instance.m_currentlyLoading) {
+ return false;
+ }
+
+ // make sure we're not exiting to desktop
+ if (Singleton.instance.m_applicationQuitting) {
+ return false;
+ }
+
+ return !paused_;
+ }
+ */
+
+ ///
+ /// Halt the Paradox Launcher
+ /// Otherwise the user will not see any compatibility warnings.
+ ///
+ private static void StopLauncherAutoContinue() {
+ Log._Debug("CompatibilityManager.StopLauncherAutoContinue()");
+
+ try {
+ autoContinue_ = LauncherLoginData.instance.m_continue;
+ LauncherLoginData.instance.m_continue = false;
+ }
+ catch (Exception e) {
+ Log.ErrorFormat(" - Stop AutoContinue Failed:\n{0}", e.ToString());
+ }
+ }
+
+ ///
+ /// Auto-load most recent save if the launcher was set to autocontinue.
+ ///
+ private static void ResumeLauncherAutoContinue() {
+ Log._DebugFormat(
+ "CompatibilityManager.ResumeLauncherAutoContinue() {0}",
+ autoContinue_);
+
+ if (paused_ || Singleton.instance.m_applicationQuitting) {
+ return;
+ }
+
+ if (autoContinue_) {
+ paused_ = true;
+ autoContinue_ = false;
+
+ try {
+ MainMenu menu = GameObject.FindObjectOfType();
+ if (menu != null) {
+ menu.m_BackgroundImage.zOrder = int.MaxValue;
+ menu.Invoke("AutoContinue", 2.5f);
+ }
+ }
+ catch (Exception e) {
+ Log.ErrorFormat(" - Resume AutoContinue Failed:\n{0}", e.ToString());
+ }
+ }
+ }
+
+ ///
+ /// Exits the game to desktop.
+ ///
+ private static void ExitToDesktop() {
+ Log._Debug("CompatibilityManager.ExitToDesktop()");
+
+ if (paused_ || Singleton.instance.m_applicationQuitting) {
+ return;
+ }
+
+ paused_ = true;
+ autoContinue_ = false;
+
+ SetEvents(false);
+
+ Singleton.instance.QuitApplication();
+ }
+ }
+}
diff --git a/TLM/TLM/Compatibility/Enum/Severity.cs b/TLM/TLM/Compatibility/Enum/Severity.cs
new file mode 100644
index 000000000..9c94a308c
--- /dev/null
+++ b/TLM/TLM/Compatibility/Enum/Severity.cs
@@ -0,0 +1,33 @@
+namespace TrafficManager.Compatibility.Enum {
+
+ ///
+ /// The severity of a mod conflict.
+ ///
+ public enum Severity {
+ ///
+ /// No known issues.
+ ///
+ None,
+
+ ///
+ /// An instance of TM:PE which is not otherwise marked as incompatible.
+ /// If there is more than one TMPE active, user must choose only one.
+ ///
+ TMPE,
+
+ ///
+ /// Minor annoyance or glitch that player can choose to live with if they want.
+ ///
+ Minor,
+
+ ///
+ /// Loss of functionality, such as a mod that directly conflicts with TM:PE.
+ ///
+ Major,
+
+ ///
+ /// Game-breaking, must be removed even if not using TM:PE.
+ ///
+ Critical,
+ }
+}
diff --git a/TLM/TLM/Compatibility/GUI/ProcessIncompatibleMods.cs b/TLM/TLM/Compatibility/GUI/ProcessIncompatibleMods.cs
new file mode 100644
index 000000000..487ba0aa5
--- /dev/null
+++ b/TLM/TLM/Compatibility/GUI/ProcessIncompatibleMods.cs
@@ -0,0 +1,274 @@
+namespace TrafficManager.Compatibility.GUI {
+ using ColossalFramework;
+ using ColossalFramework.IO;
+ using ColossalFramework.PlatformServices;
+ using static ColossalFramework.Plugins.PluginManager;
+ using ColossalFramework.UI;
+ using CSUtil.Commons;
+ using System;
+ using System.Collections.Generic;
+ using TrafficManager.Compatibility.Struct;
+ using TrafficManager.State;
+ using TrafficManager.UI;
+ using UnityEngine;
+
+ ///
+ /// Given a list of problem mods, this handles the UI to resolve those issues by
+ /// unsubscribing, disabling, etc.
+ ///
+ public class ProcessIncompatibleMods : UIPanel {
+ private UILabel title_;
+ private UIButton closeButton_;
+ private UISprite warningIcon_;
+ private UIPanel mainPanel_;
+ private UIComponent blurEffect_;
+ private UIScrollablePanel scrollPanel_;
+
+ ///
+ /// Gets or sets list of incompatible mods.
+ ///
+ public Dictionary Issues { get; set; }
+
+ ///
+ /// Initialises the dialog, populates it with list of incompatible mods, and adds it to the modal stack.
+ /// If the modal stack was previously empty, a blur effect is added over the screen background.
+ ///
+ public void Initialize() {
+ Log._Debug("IncompatibleModsPanel initialize");
+ if (mainPanel_ != null) {
+ mainPanel_.OnDestroy();
+ }
+
+ isVisible = true;
+
+ mainPanel_ = AddUIComponent();
+ mainPanel_.backgroundSprite = "UnlockingPanel2";
+ mainPanel_.color = new Color32(75, 75, 135, 255);
+ width = 600;
+ height = 440;
+ mainPanel_.width = 600;
+ mainPanel_.height = 440;
+
+ Vector2 resolution = UIView.GetAView().GetScreenResolution();
+ relativePosition = new Vector3((resolution.x / 2) - 300, resolution.y / 3);
+ mainPanel_.relativePosition = Vector3.zero;
+
+ warningIcon_ = AddWarningIcon(mainPanel_);
+
+ title_ = AddTitle(
+ mainPanel_,
+ TrafficManagerMod.ModName + " " +
+ Translation.ModConflicts.Get("Window.Title:Detected incompatible mods"));
+
+ closeButton_ = AddCloseButton(mainPanel_, CloseButtonClick);
+
+ UIPanel panel = mainPanel_.AddUIComponent();
+ panel.relativePosition = new Vector2(20, 70);
+ panel.size = new Vector2(565, 320);
+
+ /*
+ UIHelper helper = new UIHelper(mainPanel_);
+ string checkboxLabel = Translation.ModConflicts.Get("Checkbox:Scan for known incompatible mods on startup");
+ runModsCheckerOnStartup_ = helper.AddCheckbox(
+ checkboxLabel,
+ GlobalConfig.Instance.Main.ScanForKnownIncompatibleModsAtStartup,
+ RunModsCheckerOnStartup_eventCheckChanged) as UICheckBox;
+
+ runModsCheckerOnStartup_.relativePosition = new Vector3(20, height - 30f);
+ */
+
+ scrollPanel_ = AddScrollPanel(panel);
+
+ blurEffect_ = AddBlurEffect(resolution);
+
+ BringToFront();
+ }
+
+ private UISprite AddWarningIcon(UIPanel panel) {
+ UISprite sprite = panel.AddUIComponent();
+
+ sprite.spriteName = "IconWarning";
+ sprite.size = new Vector2(40f, 40f);
+ sprite.relativePosition = new Vector3(15, 15);
+ sprite.zOrder = 0;
+
+ return sprite;
+ }
+
+ private UILabel AddTitle(UIPanel panel, string titleStr) {
+ UILabel label = panel.AddUIComponent();
+
+ label.autoSize = true;
+ label.padding = new RectOffset(10, 10, 15, 15);
+ label.relativePosition = new Vector2(60, 12);
+ label.text = titleStr;
+
+ return label;
+ }
+
+ private UIButton AddCloseButton(UIPanel panel, MouseEventHandler onClick) {
+ UIButton btn = panel.AddUIComponent();
+
+ btn.eventClick += onClick;
+ btn.relativePosition = new Vector3(width - btn.width - 45, 15f);
+ btn.normalBgSprite = "buttonclose";
+ btn.hoveredBgSprite = "buttonclosehover";
+ btn.pressedBgSprite = "buttonclosepressed";
+
+ return btn;
+ }
+
+ private UIScrollablePanel AddScrollPanel(UIPanel panel) {
+ UIScrollablePanel scroll = panel.AddUIComponent();
+ scroll.backgroundSprite = string.Empty;
+ scroll.size = new Vector2(550, 340);
+ scroll.relativePosition = new Vector3(0, 0);
+ scroll.clipChildren = true;
+ scroll.autoLayoutStart = LayoutStart.TopLeft;
+ scroll.autoLayoutDirection = LayoutDirection.Vertical;
+ scroll.autoLayout = true;
+
+ /*
+ if (IncompatibleMods.Count != 0) {
+ IncompatibleMods.ForEach(
+ pair => { CreateEntry(ref scrollablePanel, pair.Value, pair.Key); });
+ }
+ */
+
+ scroll.FitTo(panel);
+ scroll.scrollWheelDirection = UIOrientation.Vertical;
+ scroll.builtinKeyNavigation = true;
+
+ UIScrollbar verticalScroll = panel.AddUIComponent();
+ verticalScroll.stepSize = 1;
+ verticalScroll.relativePosition = new Vector2(panel.width - 15, 0);
+ verticalScroll.orientation = UIOrientation.Vertical;
+ verticalScroll.size = new Vector2(20, 320);
+ verticalScroll.incrementAmount = 25;
+ verticalScroll.scrollEasingType = EasingType.BackEaseOut;
+
+ scroll.verticalScrollbar = verticalScroll;
+
+ UISlicedSprite track = verticalScroll.AddUIComponent();
+ track.spriteName = "ScrollbarTrack";
+ track.relativePosition = Vector3.zero;
+ track.size = new Vector2(16, 320);
+
+ verticalScroll.trackObject = track;
+
+ UISlicedSprite thumb = track.AddUIComponent();
+ thumb.spriteName = "ScrollbarThumb";
+ thumb.autoSize = true;
+ thumb.relativePosition = Vector3.zero;
+
+ verticalScroll.thumbObject = thumb;
+
+ return scroll;
+ }
+
+ private UIComponent AddBlurEffect(Vector2 resolution) {
+ UIComponent blur = GameObject.Find("ModalEffect").GetComponent();
+
+ AttachUIComponent(blur.gameObject);
+ blur.size = resolution;
+ blur.absolutePosition = Vector3.zero;
+ blur.SendToBack();
+ blur.eventPositionChanged += OnBlurEffectPositionChange;
+ blur.eventZOrderChanged += OnBlurEffectZOrderChange;
+ blur.opacity = 0;
+ blur.isVisible = true;
+
+ ValueAnimator.Animate(
+ "ModalEffect",
+ val => blur.opacity = val,
+ new AnimatedFloat(0f, 1f, 0.7f, EasingType.CubicEaseOut));
+
+ return blur;
+ }
+
+ private void OnBlurEffectPositionChange(UIComponent component, Vector2 position) {
+ component.absolutePosition = Vector3.zero;
+ }
+
+ private void OnBlurEffectZOrderChange(UIComponent component, int value) {
+ component.SendToBack();
+ }
+
+ ///
+ /// Allows the user to press "Esc" to close the dialog.
+ ///
+ ///
+ /// Details about the key press.
+ protected override void OnKeyDown(UIKeyEventParameter eventparam) {
+ if (Input.GetKey(KeyCode.Escape)) {
+ eventparam.Use();
+ CloseDialog();
+ } else if (Input.GetKey(KeyCode.Return)) {
+ // todo: default action
+ } else {
+ base.OnKeyDown(eventparam);
+ }
+ }
+
+ ///
+ /// Handles click of the "close dialog" button; pops the dialog off the modal stack.
+ ///
+ ///
+ /// Handle to the close button UI component (not used).
+ /// Details about the click event.
+ private void CloseButtonClick(UIComponent component, UIMouseEventParameter eventparam) {
+ eventparam.Use();
+ CloseDialog();
+ }
+
+ ///
+ /// Pops the popup dialog off the modal stack.
+ ///
+ private void CloseDialog() {
+ // remove event listeners
+ closeButton_.eventClick -= CloseButtonClick;
+ blurEffect_.eventPositionChanged += OnBlurEffectPositionChange;
+ blurEffect_.eventZOrderChanged += OnBlurEffectZOrderChange;
+
+ UIView.PopModal();
+
+ Hide();
+ Unfocus();
+
+ if (UIView.HasModalInput()) {
+ UIComponent component = UIView.GetModalComponent();
+ if (component != null) {
+ UIView.SetFocus(component);
+ }
+ } else {
+ ValueAnimator.Animate(
+ "ModalEffect",
+ val => blurEffect_.opacity = val,
+ new AnimatedFloat(1f, 0f, 0.7f, EasingType.CubicEaseOut),
+ () => blurEffect_.Hide());
+ }
+
+ // should really destroy the dialog and all child components here
+ }
+
+ ///
+ /// Deletes a locally installed mod.
+ ///
+ ///
+ /// The associated with the mod that needs deleting.
+ ///
+ /// Returns true if successfully deleted, otherwise false.
+ private bool DeleteLocalMod(PluginInfo mod) {
+ try {
+ Log.InfoFormat("Deleting local mod from {0}", mod.modPath);
+ // mod.Unload(); // this caused crash
+ DirectoryUtils.DeleteDirectory(mod.modPath);
+ return true;
+ }
+ catch (Exception e) {
+ Log.InfoFormat("- Failed:\n{0}", e.ToString());
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/TLM/TLM/Compatibility/IncompatibleMods.cs b/TLM/TLM/Compatibility/IncompatibleMods.cs
new file mode 100644
index 000000000..239faa50a
--- /dev/null
+++ b/TLM/TLM/Compatibility/IncompatibleMods.cs
@@ -0,0 +1,152 @@
+namespace TrafficManager.Compatibility {
+ using CSUtil.Commons;
+ using JetBrains.Annotations;
+ using System;
+ using System.Collections.Generic;
+ using TrafficManager.Compatibility.Enum;
+
+ ///
+ /// A list of incompatible mods.
+ ///
+ public class IncompatibleMods {
+ private static IncompatibleMods instance_;
+
+ ///
+ /// Gets the instance of the incompatible mods list.
+ ///
+ public static IncompatibleMods Instance => instance_ ?? (instance_ = new IncompatibleMods());
+
+ ///
+ /// The list of incompatible mods.
+ ///
+ public readonly Dictionary List;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public IncompatibleMods() {
+
+ Version csl_1_12_3 = new Version(1, 12, 3);
+
+ Severity severity_10_20 = Check.Versions.GetGameVersion().Equals(csl_1_12_3)
+ ? Severity.TMPE
+ : Severity.Critical;
+
+ List = new Dictionary() {
+ // Note: TM:PE v10.20 not currently listed as lots of users still use it
+ // It will be picked up by duplicate assemblies detector
+
+ // Valid versions of TM:PE
+ // If more than one is found, user will have to choose only one and the
+ // others will be removed to prevent zombie assembly save/load issues.
+ { 583429740u , severity_10_20 }, // v10.20 STABLE
+ { 1637663252u, Severity.TMPE }, // v11 STABLE
+ { 1806963141u, Severity.TMPE }, // v11 LABS
+
+ // Obsolete & rogue versions of TM:PE
+ { 1957033250u, Severity.Critical }, // Traffic Manager: President Edition (Industries Compatible)
+ { 1581695572u, Severity.Critical }, // Traffic Manager: President Edition
+ { 1546870472u, Severity.Critical }, // Traffic Manager: President Edition (Industries Compatible)
+ { 1348361731u, Severity.Critical }, // Traffic Manager: President Edition ALPHA/DEBUG
+
+ // Traffic Manager + Traffic++ AI (obsolete; game breaking)
+ { 498363759u, Severity.Critical }, // Traffic Manager + Improved AI
+ { 563720449u, Severity.Critical }, // Traffic Manager + Improved AI (Japanese Ver.)
+
+ // Traffic++ (obsolete; game breaking)
+ { 492391912u, Severity.Critical }, // Improved AI (Traffic++)
+ { 409184143u, Severity.Critical }, // Traffic++
+ { 626024868u, Severity.Critical }, // Traffic++ V2
+
+ // Extremely old verisons of Traffic Manager (obsolete; game breaking)
+ { 427585724u, Severity.Critical }, // Traffic Manager
+ { 481786333u, Severity.Critical }, // Traffic Manager Plus
+ { 568443446u, Severity.Critical }, // Traffic Manager Plus 1.2.0
+
+ // ARIS Hearse AI
+ { 433249875u, Severity.Critical }, // [ARIS] Enhanced Hearse AI
+ { 583556014u, Severity.Critical }, // Enhanced Hearse AI [Fixed for v1.4+]
+ { 813835241u, Severity.Critical }, // Enhanced Hearse AI [1.6]
+
+ // ARIS Garbage AI
+ { 439582006u, Severity.Critical }, // [ARIS] Enhanced Garbage Truck AI
+ { 583552152u, Severity.Critical }, // Enhanced Garbage Truck AI [Fixed for v1.4+]
+ { 813835391u, Severity.Critical }, // Enhanced Garbage Truck AI [1.6]
+
+ // ARIS Remove Stuck (use TM:PE "Reset Stuck Vehicles and Cims" instead)
+ { 428094792u, Severity.Critical }, // [ARIS] Remove Stuck Vehicles
+ { 587530437u, Severity.Critical }, // Remove Stuck Vehicles [Fixed for v1.4+]
+ { 813834836u, Severity.Critical }, // Remove Stuck Vehicles [1.6]
+
+ // ARIS Overwatch
+ { 421028969u, Severity.Critical }, // [ARIS] Skylines Overwatch
+ { 583538182u, Severity.Critical }, // Skylines Overwatch [Fixed for v1.3+]
+ { 813833476u, Severity.Critical }, // Skylines Overwatch [1.6]
+
+ // Old road anarchy mods (make a huge mess of networks and terrain!)
+ { 418556522u, Severity.Critical }, // Road Anarchy
+ { 954034590u, Severity.Critical }, // Road Anarchy V2
+
+ // NExt v1 (fix with "Road Removal Tool" mod)
+ { 478820060u, Severity.Critical }, // Network Extensions Project (v1)
+ { 929114228u, Severity.Critical }, // New Roads for Network Extensions
+
+ // Other game-breaking mods
+ { 408092246u, Severity.Critical }, // Traffic Report Tool 2.0
+ { 411095553u, Severity.Critical }, // Terraform Tool v0.9 (just sick of seeing this break games!)
+ { 414702884u, Severity.Critical }, // Zoneable Pedestrian Paths
+ { 417926819u, Severity.Critical }, // Road Assistant
+ { 422554572u, Severity.Critical }, // 81 Tiles Updated
+ { 436253779u, Severity.Critical }, // Road Protractor
+ { 553184329u, Severity.Critical }, // Sharp Junction Angles
+ { 651610627u, Severity.Critical }, // Road Color Changer Continued
+ { 658653260u, Severity.Critical }, // Network Nodes Editor [Experimental]
+ { 912329352u, Severity.Critical }, // Building Anarchy (just sick of seeing this break games!)
+ { 1072157697u, Severity.Critical }, // Cargo Info
+ { 1767246646u, Severity.Critical }, // AutoLineBudget (PropVehCount errors, using .Net Framework 2, published as camera script?!!)
+
+ // Incompatible with TM:PE (patch conflicts or does not fire events)
+ { 512341354u, Severity.Major }, // Central Services Dispatcher (WtM)
+ { 844180955u, Severity.Major }, // City Drive
+ { 413847191u, Severity.Major }, // SOM - Services Optimisation Module
+ { 649522495u, Severity.Major }, // District Service Limit
+ { 1803209875u, Severity.Major }, // Trees Respiration
+
+ // Mods made obsolete by TM:PE (and conflict with TM:PE patches/state)
+ { 407335588u, Severity.Major }, // No Despawn Mod
+ { 411833858u, Severity.Major }, // Toggle Traffic Lights
+ { 529979180u, Severity.Major }, // CSL Service Reserve
+ { 600733054u, Severity.Major }, // No On-Street Parking
+ { 631930385u, Severity.Major }, // Realistic Vehicle Speeds
+ { 1628112268u, Severity.Major }, // RightTurnNoStop
+
+ // Breaks toll booths
+ { 726005715u, Severity.Minor }, // Roads United Core+
+ { 680748394u, Severity.Minor }, // Roads United: North America
+
+ // Reported to cause lane usage issues in TM:PE
+ { 810858473u, Severity.Minor }, // Traffic Report Mod: Updated
+
+ // Obsolete by vanilla game functionality (also, does not fire events we need to maintain state)
+ { 631694768u, Severity.Minor }, // Extended Road Upgrade
+ { 408209297u, Severity.Minor }, // Extended Road Upgrade
+
+ // Obsolete & outdated - use vanilla functionality, or Improved Stop Selection mod
+ { 532863263u, Severity.Minor }, // Multi-track Station Enabler
+ { 442957897u, Severity.Minor }, // Multi-track Station Enabler
+ };
+
+ Log.InfoFormat(
+ "Compatibility.IncompatibleMods.List contains {0} item(s)",
+ List.Count);
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ [UsedImplicitly]
+ ~IncompatibleMods() {
+ List.Clear();
+ }
+ }
+}
diff --git a/TLM/TLM/Compatibility/Struct/ModDescriptor.cs b/TLM/TLM/Compatibility/Struct/ModDescriptor.cs
new file mode 100644
index 000000000..c637c60bf
--- /dev/null
+++ b/TLM/TLM/Compatibility/Struct/ModDescriptor.cs
@@ -0,0 +1,135 @@
+namespace TrafficManager.Compatibility.Struct {
+ using static ColossalFramework.Plugins.PluginManager;
+ using System;
+ using System.IO;
+ using System.Reflection;
+ using TrafficManager.Compatibility.Enum;
+ using TrafficManager.Compatibility.Util;
+
+ ///
+ /// Descriptor for subscribed/local mods.
+ ///
+ public struct ModDescriptor {
+ ///
+ /// Assembly name (of the IUserMod assembly).
+ ///
+ public readonly string AssemblyName;
+
+ ///
+ /// Assembly version (of the IUserMod assembly).
+ ///
+ public readonly Version AssemblyVersion;
+
+ ///
+ /// Assembly Guid (of the IUserMod assembly).
+ ///
+ public readonly Guid AssemblyGuid;
+
+ ///
+ /// Value of Name property of the IUserMod.
+ ///
+ public readonly string ModName;
+
+ ///
+ /// Workshop ID of the IUserMod.
+ ///
+ public readonly ulong ModWorkshopId;
+
+ ///
+ /// If true, the mod is locally installed (not a workshop subscription).
+ ///
+ public readonly bool ModIsLocal;
+
+ ///
+ /// Incompatibility severity of the mod.
+ ///
+ public readonly Severity Incompatibility;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ ///
+ /// Assembly name.
+ /// Assembly version.
+ /// Assembly guid.
+ /// Mod name.
+ /// Mod workshop ID.
+ /// Is mod local?
+ /// Compatibility severity.
+ public ModDescriptor(
+ string asmName,
+ Version asmVer,
+ Guid asmGuid,
+ string modName,
+ ulong modId,
+ bool modLocal,
+ Severity sev) {
+
+ AssemblyName = asmName;
+ AssemblyVersion = asmVer;
+ AssemblyGuid = asmGuid;
+ ModName = modName;
+ ModWorkshopId = modId;
+ ModIsLocal = modLocal;
+ Incompatibility = sev;
+ }
+
+ ///
+ /// Generates descriptor from a .
+ ///
+ ///
+ /// The to inspect.
+ public static implicit operator ModDescriptor(PluginInfo mod) {
+ return From(mod);
+ }
+
+ private static ModDescriptor From(PluginInfo mod) {
+ Assembly asm = ModInspector.GetModAssembly(mod);
+
+ string asmName = ModInspector.GetAssemblyName(asm);
+
+ Guid asmGuid = ModInspector.GetAssemblyGuid(asm);
+
+ ulong modId = mod?.publishedFileID.AsUInt64 ?? 0;
+
+ // If workshop id is ulong.MaxValue, it's a locally installed mod
+ bool modLocal = modId == ulong.MaxValue;
+
+ string modName = modLocal
+ ? $"{ModInspector.GetModName(mod)} /{Path.GetFileName(mod.modPath)}"
+ : ModInspector.GetModName(mod);
+
+ Severity severity;
+
+ if (IncompatibleMods.Instance.List.TryGetValue(modId, out Severity s)) {
+ severity = s;
+ } else if (asmName == "TrafficManager") {
+ // Detect currently unknown or local builds of TM:PE.
+ // Assume anything newer than v11 LABS (aubergine18) is safe,
+ // anything older is rogue or obsolete.
+ // Local builds are treated as newer due to ulong.MaxValue id.
+ severity = modId > 1806963141u
+ ? Severity.TMPE
+ : Severity.Critical;
+ } else {
+ severity = Severity.None;
+ }
+
+ // Show Guid for potentially valid TM:PE mods.
+ if (severity == Severity.TMPE) {
+ modName += $" - {asmGuid}";
+ }
+
+ return new ModDescriptor(
+ asmName,
+ ModInspector.GetAssemblyVersion(asm),
+ asmGuid,
+
+ modName,
+ modId,
+ modLocal,
+
+ severity);
+ }
+ }
+}
diff --git a/TLM/TLM/Compatibility/Util/MemberValue.cs b/TLM/TLM/Compatibility/Util/MemberValue.cs
new file mode 100644
index 000000000..52980d80c
--- /dev/null
+++ b/TLM/TLM/Compatibility/Util/MemberValue.cs
@@ -0,0 +1,80 @@
+namespace TrafficManager.Util {
+ using System;
+ using System.Linq;
+ using System.Reflection;
+
+ ///
+ /// A nicer way to retrieve values of class instance members.
+ /// Derived from: https://stackoverflow.com/questions/60236067/
+ ///
+ public class MemberValue {
+ ///
+ /// Retrieves the value from a field/method/property member of an object.
+ ///
+ ///
+ /// The value .
+ ///
+ /// An instance.
+ /// The for the member to inspect.
+ ///
+ /// Returns the value of the , if found, otherwise the default value for .
+ public static T GetValue(object instance, MemberInfo member) {
+ return member.MemberType switch {
+ MemberTypes.Field => (T)(member as FieldInfo)?.GetValue(instance),
+ MemberTypes.Method => (T)(member as MethodInfo)?.Invoke(instance, null),
+ MemberTypes.Property => (T)(member as PropertyInfo)?.GetValue(instance, null),
+ _ => default,
+ };
+ }
+
+ ///
+ /// Tries to get the value of a member of an object instance.
+ ///
+ ///
+ /// The value .
+ ///
+ /// The of the .
+ /// An instance.
+ /// The name (case insensitive) of the member to inspect.
+ /// The retrieved member value, if successful.
+ ///
+ /// Returns true if successful, otherwise false.
+ ///
+ ///
+ /// Type type = asm.GetType("TrafficManager.TrafficManagerMod");
+ /// object instance = Activator.CreateInstance(type);
+ ///
+ /// string branch;
+ ///
+ /// if (TryGetMemberValue(type, instance, "BRANCH", out string val)) {
+ /// branch = val;
+ /// }
+ ///
+ /// (instance as IDisposable)?.Dispose();
+ ///
+ public static bool TryGetMemberValue(Type type, object instance, string name, out T value) {
+ value = default;
+
+ if (instance == null) {
+ return false;
+ }
+
+ try {
+ var member = type
+ .GetMember(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.IgnoreCase)
+ .FirstOrDefault();
+
+ if (member == null) {
+ return false;
+ }
+
+ value = GetValue(instance, member);
+ }
+ catch {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/TLM/TLM/Compatibility/Util/ModInspector.cs b/TLM/TLM/Compatibility/Util/ModInspector.cs
new file mode 100644
index 000000000..ec6997b51
--- /dev/null
+++ b/TLM/TLM/Compatibility/Util/ModInspector.cs
@@ -0,0 +1,111 @@
+namespace TrafficManager.Compatibility.Util {
+ using static ColossalFramework.Plugins.PluginManager;
+ using System;
+ using System.Reflection;
+ using TrafficManager.Compatibility.Struct;
+ using ICities;
+ using CSUtil.Commons;
+ using TrafficManager.Compatibility.Enum;
+
+ public class ModInspector {
+
+ ///
+ /// Game always uses ulong.MaxValue to depict local mods.
+ ///
+ internal const ulong LOCAL_MOD_ID = ulong.MaxValue;
+
+ ///
+ /// Obtain the for a .
+ ///
+ ///
+ /// The to inspect.
+ ///
+ /// Returns the if successful, otherwise null.
+ internal static Assembly GetModAssembly(PluginInfo mod) {
+ return mod?.userModInstance?.GetType().Assembly;
+ }
+
+ ///
+ /// Returns the name of an .
+ ///
+ ///
+ /// The to inspect.
+ ///
+ /// Returns the name if successful, otherwise null.
+ internal static string GetAssemblyName(Assembly asm) {
+ return asm?.GetName().Name;
+ }
+
+ ///
+ /// Obtain the assembly name for a .
+ ///
+ ///
+ /// The to inspect.
+ ///
+ /// Returns the name if successful, otherwise null.
+ internal static string GetAssemblyName(PluginInfo mod) {
+ return GetAssemblyName(GetModAssembly(mod));
+ }
+
+ ///
+ /// Obtain the for an .
+ ///
+ ///
+ /// The to inspect.
+ ///
+ /// Returns the if successful, otherwise default.
+ internal static Guid GetAssemblyGuid(Assembly asm) {
+ return asm?.ManifestModule.ModuleVersionId ?? default;
+ }
+
+ ///
+ /// Obtain the assembly for a .
+ ///
+ ///
+ /// The to inspect.
+ ///
+ /// Returns the if successful, otherwise default.
+ internal static Guid GetAssemblyGuid(PluginInfo mod) {
+ return GetAssemblyGuid(GetModAssembly(mod));
+ }
+
+ internal static Version GetAssemblyVersion(Assembly asm) {
+ return asm?.GetName().Version;
+ }
+
+ internal static Version GetAssemblyVersion(PluginInfo mod) {
+ return GetAssemblyVersion(GetModAssembly(mod));
+ }
+
+
+
+ internal static string GetModName(PluginInfo mod) {
+ return mod?.userModInstance != null ? ((IUserMod)mod.userModInstance).Name : string.Empty;
+ }
+
+ ///
+ /// ONLY USE FOR MODS WE KNOW DON'T HAVE STATIC OR INSTANCE CONSTRUCTORS ON THEIR IUSERMOD CLASS.
+ ///
+ ///
+ /// The to inspect.
+ ///
+ internal static Version GetModVersion(PluginInfo mod) {
+ if (TryGetModVersion(mod, out Version ver)) {
+ return ver;
+ } else {
+ return default;
+ }
+ }
+
+ internal static bool TryGetModVersion(PluginInfo mod, out Version ver) {
+ ver = default;
+
+ if (mod == null || mod.userModInstance == null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ }
+}
diff --git a/TLM/TLM/LoadingExtension.cs b/TLM/TLM/LoadingExtension.cs
index bf3b4b7ac..9dc6f77eb 100644
--- a/TLM/TLM/LoadingExtension.cs
+++ b/TLM/TLM/LoadingExtension.cs
@@ -21,6 +21,7 @@ namespace TrafficManager {
[UsedImplicitly]
public class LoadingExtension : LoadingExtensionBase {
+
private const string HARMONY_ID = "de.viathinksoft.tmpe";
internal static LoadingExtension Instance = null;
@@ -273,7 +274,7 @@ private void InitDetours() {
}
public override void OnCreated(ILoading loading) {
- Log._Debug("LoadingExtension.OnCreated() called");
+ Log._Debug("LoadingExtension.OnCreated()");
// SelfDestruct.DestructOldInstances(this);
base.OnCreated(loading);
@@ -321,12 +322,13 @@ private void RegisterCustomManagers() {
}
public override void OnReleased() {
+ Log.Info("LoadingExension.OnReleased()");
Instance = null;
base.OnReleased();
}
public override void OnLevelUnloading() {
- Log.Info("OnLevelUnloading");
+ Log.Info("LoadingExtension.OnLevelUnloading()");
base.OnLevelUnloading();
CustomPathManager._instance.WaitForAllPaths();
@@ -402,73 +404,6 @@ public override void OnLevelLoaded(LoadMode mode) {
case SimulationManager.UpdateMode.NewGameFromMap:
case SimulationManager.UpdateMode.NewGameFromScenario:
case SimulationManager.UpdateMode.LoadGame: {
- if (BuildConfig.applicationVersion != BuildConfig.VersionToString(
- TrafficManagerMod.GAME_VERSION,
- false))
- {
- string[] majorVersionElms = BuildConfig.applicationVersion.Split('-');
- string[] versionElms = majorVersionElms[0].Split('.');
- uint versionA = Convert.ToUInt32(versionElms[0]);
- uint versionB = Convert.ToUInt32(versionElms[1]);
- uint versionC = Convert.ToUInt32(versionElms[2]);
-
- Log.Info($"Detected game version v{BuildConfig.applicationVersion}");
-
- bool isModTooOld = TrafficManagerMod.GAME_VERSION_A < versionA ||
- (TrafficManagerMod.GAME_VERSION_A == versionA &&
- TrafficManagerMod.GAME_VERSION_B < versionB);
- // || (TrafficManagerMod.GameVersionA == versionA
- // && TrafficManagerMod.GameVersionB == versionB
- // && TrafficManagerMod.GameVersionC < versionC);
-
- bool isModNewer = TrafficManagerMod.GAME_VERSION_A < versionA ||
- (TrafficManagerMod.GAME_VERSION_A == versionA &&
- TrafficManagerMod.GAME_VERSION_B > versionB);
- // || (TrafficManagerMod.GameVersionA == versionA
- // && TrafficManagerMod.GameVersionB == versionB
- // && TrafficManagerMod.GameVersionC > versionC);
-
- if (isModTooOld) {
- string msg = string.Format(
- "Traffic Manager: President Edition detected that you are running " +
- "a newer game version ({0}) than TM:PE has been built for ({1}). " +
- "Please be aware that TM:PE has not been updated for the newest game " +
- "version yet and thus it is very likely it will not work as expected.",
- BuildConfig.applicationVersion,
- BuildConfig.VersionToString(TrafficManagerMod.GAME_VERSION, false));
-
- Log.Error(msg);
- Singleton.instance.m_ThreadingWrapper.QueueMainThread(
- () => {
- UIView.library
- .ShowModal("ExceptionPanel")
- .SetMessage(
- "TM:PE has not been updated yet",
- msg,
- false);
- });
- } else if (isModNewer) {
- string msg = string.Format(
- "Traffic Manager: President Edition has been built for game version {0}. " +
- "You are running game version {1}. Some features of TM:PE will not " +
- "work with older game versions. Please let Steam update your game.",
- BuildConfig.VersionToString(TrafficManagerMod.GAME_VERSION, false),
- BuildConfig.applicationVersion);
-
- Log.Error(msg);
- Singleton
- .instance.m_ThreadingWrapper.QueueMainThread(
- () => {
- UIView.library
- .ShowModal("ExceptionPanel")
- .SetMessage(
- "Your game should be updated",
- msg,
- false);
- });
- }
- }
-
IsGameLoaded = true;
break;
}
diff --git a/TLM/TLM/Resources/incompatible_mods.txt b/TLM/TLM/Resources/incompatible_mods.txt
deleted file mode 100644
index 8f0d64d8b..000000000
--- a/TLM/TLM/Resources/incompatible_mods.txt
+++ /dev/null
@@ -1,53 +0,0 @@
-1957033250;Traffic Manager: President Edition (Industries Compatible)
-1546870472;Traffic Manager: President Edition (Industries Compatible)
-1581695572;Traffic Manager: President Edition
-1348361731;Traffic Manager: President Edition ALPHA/DEBUG
-498363759;Traffic Manager + Improved AI
-563720449;Traffic Manager + Improved AI (Japanese Ver.)
-492391912;Improved AI (Traffic++)
-409184143;Traffic++
-626024868;Traffic++ V2
-568443446;Traffic Manager Plus 1.2.0
-481786333;Traffic Manager Plus
-427585724;Traffic Manager
-407335588;No Despawn Mod
-600733054;No On-Street Parking
-1628112268;RightTurnNoStop
-411833858;Toggle Traffic Lights
-512341354;Central Services Dispatcher (WtM)
-844180955;City Drive
-529979180;CSL Service Reserve
-433249875;[ARIS] Enhanced Hearse AI
-583556014;Enhanced Hearse AI [Fixed for v1.4+]
-813835241;Enhanced Hearse AI [1.6]
-439582006;[ARIS] Enhanced Garbage Truck AI
-583552152;Enhanced Garbage Truck AI [Fixed for v1.4+]
-813835391;Enhanced Garbage Truck AI [1.6]
-413847191;SOM - Services Optimisation Module
-418556522;Road Anarchy
-954034590;Road Anarchy V2
-726005715;Roads United Core+
-680748394;Roads United: North America
-532863263;Multi-track Station Enabler
-442957897;Multi-track Station Enabler
-553184329;Sharp Junction Angles
-478820060;Network Extensions Project (v1)
-658653260;Network Nodes Editor [Experimental]
-929114228;New Roads for Network Extensions
-436253779;Road Protractor
-651610627;Road Color Changer Continued
-422554572;81 Tiles Updated
-414702884;Zoneable Pedestrian Paths
-631694768;Extended Road Upgrade
-408209297;Extended Road Upgrade
-649522495;District Service Limit
-428094792;[ARIS] Remove Stuck Vehicles
-587530437;Remove Stuck Vehicles [Fixed for v1.4+]
-813834836;Remove Stuck Vehicles [1.6]
-421028969;[ARIS] Skylines Overwatch
-583538182;Skylines Overwatch [Fixed for v1.3+]
-813833476;Skylines Overwatch [1.6]
-417926819;Road Assistant
-631930385;Realistic Vehicle Speeds
-1072157697;Cargo Info
-1803209875;Trees Respiration
\ No newline at end of file
diff --git a/TLM/TLM/TLM.csproj b/TLM/TLM/TLM.csproj
index 2d1f937e5..a1ce0bdec 100644
--- a/TLM/TLM/TLM.csproj
+++ b/TLM/TLM/TLM.csproj
@@ -1,4 +1,4 @@
-
+
@@ -126,6 +126,13 @@
Properties\SharedAssemblyInfo.cs
+
+
+
+
+
+
+
@@ -149,6 +156,7 @@
+
@@ -245,7 +253,7 @@
-
+
@@ -297,6 +305,7 @@
+
@@ -304,7 +313,7 @@
-
+
@@ -415,9 +424,6 @@
-
-
-
{f8759084-df5b-4a54-b73c-824640a8fa3f}
@@ -571,7 +577,7 @@ xcopy /y "$(TargetDir)TMPE.RedirectionFramework.dll" "%25DEPLOYDIR%25"
xcopy /y "$(TargetDir)CSUtil.Commons.dll" "%25DEPLOYDIR%25"
xcopy /y "$(TargetDir)0TMPE.Harmony.dll" "%25DEPLOYDIR%25"
echo THE ASSEMBLY VERSION IS: @(VersionNumber) created at %25time%25
-
+
set DEPLOYDIR=
diff --git a/TLM/TLM/TrafficManagerMod.cs b/TLM/TLM/TrafficManagerMod.cs
index 099646550..f17f69924 100644
--- a/TLM/TLM/TrafficManagerMod.cs
+++ b/TLM/TLM/TrafficManagerMod.cs
@@ -1,69 +1,84 @@
namespace TrafficManager {
using ColossalFramework.Globalization;
- using ColossalFramework.UI;
using CSUtil.Commons;
using ICities;
using JetBrains.Annotations;
using System.Reflection;
using System;
+ using TrafficManager.Compatibility;
using TrafficManager.State;
using TrafficManager.UI;
- using TrafficManager.Util;
- using static TrafficManager.Util.Shortcuts;
- using ColossalFramework;
+ using UnityEngine.SceneManagement;
+ ///
+ /// The main class of the mod, which gets instantiated by the game engine.
+ ///
public class TrafficManagerMod : IUserMod {
#if LABS
+ ///
+ /// Build configuration RELEASE LABS.
+ ///
public const string BRANCH = "LABS";
#elif DEBUG
+ ///
+ /// Build configuration DEBUG, TRACE, etc.
+ ///
public const string BRANCH = "DEBUG";
#else
+ ///
+ /// Build configuration RELEASE.
+ ///
public const string BRANCH = "STABLE";
#endif
- // These values from `BuildConfig` class (`APPLICATION_VERSION` constants) in game file `Managed/Assembly-CSharp.dll` (use ILSpy to inspect them)
- public const uint GAME_VERSION = 185066000u;
- public const uint GAME_VERSION_A = 1u;
- public const uint GAME_VERSION_B = 12u;
- public const uint GAME_VERSION_C = 3u;
- public const uint GAME_VERSION_BUILD = 2u;
+ ///
+ /// Defines the game version that this version of TM:PE is expecting.
+ /// See for more info.
+ /// Update when necessary.
+ ///
+ public static readonly Version ExpectedGameVersion = new Version(1, 12, 3);
- // Use SharedAssemblyInfo.cs to modify TM:PE version
- // External mods (eg. CSUR Toolbox) reference the versioning for compatibility purposes
+ ///
+ /// The full mod name including version number and branch. This is also shown on the TM:PE toolbar in-game.
+ ///
+ public static readonly string ModName = "TM:PE " + VersionString + " " + BRANCH;
+
+ ///
+ /// Gets the mod version as defined by SharedAssemblyInfo.cs.
+ /// Update with each release to workshop.
+ ///
public static Version ModVersion => typeof(TrafficManagerMod).Assembly.GetName().Version;
- // used for in-game display
+ ///
+ /// Gets the string represetnation of .
+ ///
public static string VersionString => ModVersion.ToString(3);
- public static readonly string ModName = "TM:PE " + VersionString + " " + BRANCH;
-
+ ///
+ /// Gets the mod name, which is shown in Content Manager > Mods, and also Options > Mod Settings.
+ ///
+ [UsedImplicitly]
public string Name => ModName;
+ ///
+ /// Gets the description of the mod shown in Content Manager > Mods.
+ ///
+ [UsedImplicitly]
public string Description => "Manage your city's traffic";
+ ///
+ /// This method is called by the game when the mod is enabled.
+ ///
[UsedImplicitly]
public void OnEnabled() {
Log.InfoFormat(
- "TM:PE enabled. Version {0}, Build {1} {2} for game version {3}.{4}.{5}-f{6}",
- VersionString,
- Assembly.GetExecutingAssembly().GetName().Version,
- BRANCH,
- GAME_VERSION_A,
- GAME_VERSION_B,
- GAME_VERSION_C,
- GAME_VERSION_BUILD);
+ "{0} designed for Cities: Skylines {1}",
+ ModName,
+ ExpectedGameVersion.ToString(3));
+
Log.InfoFormat(
- "Enabled TM:PE has GUID {0}",
- Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId);
-
- // check for incompatible mods
- if (UIView.GetAView() != null) {
- // when TM:PE is enabled in content manager
- CheckForIncompatibleMods();
- } else {
- // or when game first loads if TM:PE was already enabled
- LoadingManager.instance.m_introLoaded += CheckForIncompatibleMods;
- }
+ "TrafficManagerMod.OnEnabled() Scene = {0}",
+ SceneManager.GetActiveScene().name);
// Log Mono version
Type monoRt = Type.GetType("Mono.Runtime");
@@ -75,12 +90,22 @@ public void OnEnabled() {
Log.InfoFormat("Mono version: {0}", displayName.Invoke(null, null));
}
}
+
+ // Run pre-flight compatibility checks
+ CompatibilityManager.Activate();
}
+ ///
+ /// This method is called by the game when the mod is disabled.
+ ///
[UsedImplicitly]
public void OnDisabled() {
- Log.Info("TM:PE disabled.");
- LoadingManager.instance.m_introLoaded -= CheckForIncompatibleMods;
+ Log.InfoFormat(
+ "TrafficManagerMod.OnDisabled() Scene = {0}",
+ SceneManager.GetActiveScene().name);
+
+ CompatibilityManager.Deactivate();
+
LocaleManager.eventLocaleChanged -= Translation.HandleGameLocaleChange;
Translation.IsListeningToGameLocaleChanged = false; // is this necessary?
@@ -91,8 +116,16 @@ public void OnDisabled() {
}
}
+ ///
+ /// This method is called by the game to initialise the mod settings screen.
+ ///
+ /// A helper for creating UI components.
[UsedImplicitly]
public void OnSettingsUI(UIHelperBase helper) {
+ Log.InfoFormat(
+ "TrafficManagerMod.OnSettingsUI() Scene = {0}",
+ SceneManager.GetActiveScene().name);
+
// Note: This bugs out if done in OnEnabled(), hence doing it here instead.
if (!Translation.IsListeningToGameLocaleChanged) {
Translation.IsListeningToGameLocaleChanged = true;
@@ -100,10 +133,5 @@ public void OnSettingsUI(UIHelperBase helper) {
}
Options.MakeSettings(helper);
}
-
- private static void CheckForIncompatibleMods() {
- ModsCompatibilityChecker mcc = new ModsCompatibilityChecker();
- mcc.PerformModCheck();
- }
}
}
diff --git a/TLM/TLM/UI/IncompatibleModsPanel.cs b/TLM/TLM/UI/IncompatibleModsPanel.cs
deleted file mode 100644
index 96e093728..000000000
--- a/TLM/TLM/UI/IncompatibleModsPanel.cs
+++ /dev/null
@@ -1,343 +0,0 @@
-namespace TrafficManager.UI {
- using ColossalFramework.IO;
- using ColossalFramework.PlatformServices;
- using ColossalFramework.UI;
- using ColossalFramework;
- using CSUtil.Commons;
- using static ColossalFramework.Plugins.PluginManager;
- using System.Collections.Generic;
- using System;
- using TrafficManager.State;
- using UnityEngine;
-
- public class IncompatibleModsPanel : UIPanel {
- private const ulong LOCAL_MOD = ulong.MaxValue;
-
- private UILabel title_;
- private UIButton closeButton_;
- private UISprite warningIcon_;
- private UIPanel mainPanel_;
- private UICheckBox runModsCheckerOnStartup_;
- private UIComponent blurEffect_;
-
- ///
- /// Gets or sets list of incompatible mods from
- /// .
- ///
- public Dictionary IncompatibleMods { get; set; }
-
- ///
- /// Initialises the dialog, populates it with list of incompatible mods, and adds it to the modal stack.
- /// If the modal stack was previously empty, a blur effect is added over the screen background.
- ///
- public void Initialize() {
- Log._Debug("IncompatibleModsPanel initialize");
- if (mainPanel_ != null) {
- mainPanel_.OnDestroy();
- }
-
- isVisible = true;
-
- mainPanel_ = AddUIComponent();
- mainPanel_.backgroundSprite = "UnlockingPanel2";
- mainPanel_.color = new Color32(75, 75, 135, 255);
- width = 600;
- height = 440;
- mainPanel_.width = 600;
- mainPanel_.height = 440;
-
- Vector2 resolution = UIView.GetAView().GetScreenResolution();
- relativePosition = new Vector3((resolution.x / 2) - 300, resolution.y / 3);
- mainPanel_.relativePosition = Vector3.zero;
-
- warningIcon_ = mainPanel_.AddUIComponent();
- warningIcon_.size = new Vector2(40f, 40f);
- warningIcon_.spriteName = "IconWarning";
- warningIcon_.relativePosition = new Vector3(15, 15);
- warningIcon_.zOrder = 0;
-
- title_ = mainPanel_.AddUIComponent();
- title_.autoSize = true;
- title_.padding = new RectOffset(10, 10, 15, 15);
- title_.relativePosition = new Vector2(60, 12);
-
- title_.text = TrafficManagerMod.ModName + " " +
- Translation.ModConflicts.Get("Window.Title:Detected incompatible mods");
-
- closeButton_ = mainPanel_.AddUIComponent();
- closeButton_.eventClick += CloseButtonClick;
- closeButton_.relativePosition = new Vector3(width - closeButton_.width - 45, 15f);
- closeButton_.normalBgSprite = "buttonclose";
- closeButton_.hoveredBgSprite = "buttonclosehover";
- closeButton_.pressedBgSprite = "buttonclosepressed";
-
- UIPanel panel = mainPanel_.AddUIComponent();
- panel.relativePosition = new Vector2(20, 70);
- panel.size = new Vector2(565, 320);
-
- UIHelper helper = new UIHelper(mainPanel_);
- string checkboxLabel = Translation.ModConflicts.Get("Checkbox:Scan for known incompatible mods on startup");
- runModsCheckerOnStartup_ = helper.AddCheckbox(
- checkboxLabel,
- GlobalConfig.Instance.Main.ScanForKnownIncompatibleModsAtStartup,
- RunModsCheckerOnStartup_eventCheckChanged) as UICheckBox;
- runModsCheckerOnStartup_.relativePosition = new Vector3(20, height - 30f);
-
- UIScrollablePanel scrollablePanel = panel.AddUIComponent();
- scrollablePanel.backgroundSprite = string.Empty;
- scrollablePanel.size = new Vector2(550, 340);
- scrollablePanel.relativePosition = new Vector3(0, 0);
- scrollablePanel.clipChildren = true;
- scrollablePanel.autoLayoutStart = LayoutStart.TopLeft;
- scrollablePanel.autoLayoutDirection = LayoutDirection.Vertical;
- scrollablePanel.autoLayout = true;
-
- // Populate list of incompatible mods
- if (IncompatibleMods.Count != 0) {
- IncompatibleMods.ForEach(
- pair => { CreateEntry(ref scrollablePanel, pair.Value, pair.Key); });
- }
-
- scrollablePanel.FitTo(panel);
- scrollablePanel.scrollWheelDirection = UIOrientation.Vertical;
- scrollablePanel.builtinKeyNavigation = true;
-
- UIScrollbar verticalScroll = panel.AddUIComponent();
- verticalScroll.stepSize = 1;
- verticalScroll.relativePosition = new Vector2(panel.width - 15, 0);
- verticalScroll.orientation = UIOrientation.Vertical;
- verticalScroll.size = new Vector2(20, 320);
- verticalScroll.incrementAmount = 25;
- verticalScroll.scrollEasingType = EasingType.BackEaseOut;
-
- scrollablePanel.verticalScrollbar = verticalScroll;
-
- UISlicedSprite track = verticalScroll.AddUIComponent();
- track.spriteName = "ScrollbarTrack";
- track.relativePosition = Vector3.zero;
- track.size = new Vector2(16, 320);
-
- verticalScroll.trackObject = track;
-
- UISlicedSprite thumb = track.AddUIComponent();
- thumb.spriteName = "ScrollbarThumb";
- thumb.autoSize = true;
- thumb.relativePosition = Vector3.zero;
- verticalScroll.thumbObject = thumb;
-
- // Add blur effect if applicable
- blurEffect_ = GameObject.Find("ModalEffect").GetComponent();
- AttachUIComponent(blurEffect_.gameObject);
- blurEffect_.size = new Vector2(resolution.x, resolution.y);
- blurEffect_.absolutePosition = new Vector3(0, 0);
- blurEffect_.SendToBack();
- blurEffect_.eventPositionChanged += OnBlurEffectPositionChange;
- blurEffect_.eventZOrderChanged += OnBlurEffectZOrderChange;
- blurEffect_.opacity = 0;
- blurEffect_.isVisible = true;
- ValueAnimator.Animate(
- "ModalEffect",
- val => blurEffect_.opacity = val,
- new AnimatedFloat(0f, 1f, 0.7f, EasingType.CubicEaseOut));
-
- // Make sure modal dialog is in front of all other UI
- BringToFront();
- }
-
- private void OnBlurEffectPositionChange(UIComponent component, Vector2 position) {
- blurEffect_.absolutePosition = Vector3.zero;
- }
-
- private void OnBlurEffectZOrderChange(UIComponent component, int value) {
- blurEffect_.zOrder = 0;
- mainPanel_.zOrder = 1000;
- }
-
- ///
- /// Allows the user to press "Esc" to close the dialog.
- ///
- ///
- /// Details about the key press.
- protected override void OnKeyDown(UIKeyEventParameter p) {
- if (Input.GetKey(KeyCode.Escape) || Input.GetKey(KeyCode.Return)) {
- TryPopModal();
- p.Use();
- Hide();
- Unfocus();
- }
-
- base.OnKeyDown(p);
- }
-
- ///
- /// Hnadles click of the "Run incompatible check on startup" checkbox and updates game options accordingly.
- ///
- ///
- /// The new value of the checkbox; true if checked, otherwise false.
- private void RunModsCheckerOnStartup_eventCheckChanged(bool value) {
- Log._Debug("Incompatible mods checker run on game launch changed to " + value);
- OptionsGeneralTab.SetScanForKnownIncompatibleMods(value);
- }
-
- ///
- /// Handles click of the "close dialog" button; pops the dialog off the modal stack.
- ///
- ///
- /// Handle to the close button UI component (not used).
- /// Details about the click event.
- private void CloseButtonClick(UIComponent component, UIMouseEventParameter eventparam) {
- CloseDialog();
- eventparam.Use();
- }
-
- ///
- /// Pops the popup dialog off the modal stack.
- ///
- private void CloseDialog() {
- closeButton_.eventClick -= CloseButtonClick;
- TryPopModal();
- Hide();
- Unfocus();
- }
-
- ///
- /// Creates a panel representing the mod and adds it to the UI component.
- ///
- ///
- /// The parent UI component that the panel will be added to.
- /// The name of the mod, which is displayed to user.
- /// The instance of the incompatible mod.
- private void CreateEntry(ref UIScrollablePanel parent, string modName, PluginInfo mod) {
- string caption = mod.publishedFileID.AsUInt64 == LOCAL_MOD
- ? Translation.ModConflicts.Get("Button:Delete mod")
- : Translation.ModConflicts.Get("Button:Unsubscribe mod");
-
- UIPanel panel = parent.AddUIComponent();
- panel.size = new Vector2(560, 50);
- panel.backgroundSprite = "ContentManagerItemBackground";
-
- UILabel label = panel.AddUIComponent();
- label.text = modName;
- label.textAlignment = UIHorizontalAlignment.Left;
- label.relativePosition = new Vector2(10, 15);
-
- CreateButton(
- panel,
- caption,
- (int)panel.width - 170,
- 10,
- (component, param) => UnsubscribeClick(component, param, mod));
- }
-
- ///
- /// Handles click of "Unsubscribe" or "Delete" button; removes the associated mod and updates UI.
- ///
- /// Once all incompatible mods are removed, the dialog will be closed automatically.
- ///
- ///
- /// A handle to the UI button that was clicked.
- /// Details of the click event.
- /// The instance of the mod to remove.
- private void UnsubscribeClick(UIComponent component,
- UIMouseEventParameter eventparam,
- PluginInfo mod) {
- eventparam.Use();
- bool success;
-
- // disable button to prevent accidental clicks
- component.isEnabled = false;
- Log.Info($"Removing incompatible mod '{mod.name}' from {mod.modPath}");
-
- success = mod.publishedFileID.AsUInt64 == LOCAL_MOD
- ? DeleteLocalTMPE(mod)
- : PlatformService.workshop.Unsubscribe(mod.publishedFileID);
-
- if (success) {
- IncompatibleMods.Remove(mod);
- component.parent.Disable();
- component.isVisible = false;
-
- // automatically close the dialog if no more mods to remove
- if (IncompatibleMods.Count == 0) {
- CloseDialog();
- }
- } else {
- Log.Warning($"Failed to remove mod '{mod.name}'");
- component.isEnabled = true;
- }
- }
-
- ///
- /// Deletes a locally installed TM:PE mod.
- ///
- /// The associated with the mod that needs deleting.
- /// Returns true if successfully deleted, otherwise false.
- private bool DeleteLocalTMPE(PluginInfo mod) {
- try {
- Log._Debug($"Deleting local TM:PE from {mod.modPath}");
- // mod.Unload();
- DirectoryUtils.DeleteDirectory(mod.modPath);
- return true;
- }
- catch (Exception e) {
- return false;
- }
- }
-
- ///
- /// Creates an `Unsubscribe` or `Delete` button (as applicable to mod location) and attaches
- /// it to the UI component.
- ///
- /// The parent UI component which the button will be attached to.
- /// The translated text to display on the button.
- /// The x position of the top-left corner of the button, relative to
- /// .
- /// The y position of the top-left corner of the button, relative to
- /// .
- /// The event handler for when the button is clicked.
- private void CreateButton(UIComponent parent,
- string text,
- int x,
- int y,
- MouseEventHandler eventClick) {
- var button = parent.AddUIComponent();
- button.textScale = 0.8f;
- button.width = 150f;
- button.height = 30;
- button.normalBgSprite = "ButtonMenu";
- button.disabledBgSprite = "ButtonMenuDisabled";
- button.hoveredBgSprite = "ButtonMenuHovered";
- button.focusedBgSprite = "ButtonMenu";
- button.pressedBgSprite = "ButtonMenuPressed";
- button.textColor = new Color32(255, 255, 255, 255);
- button.playAudioEvents = true;
- button.text = text;
- button.relativePosition = new Vector3(x, y);
- button.eventClick += eventClick;
- }
-
- ///
- /// Pops the dialog from the modal stack. If no more modal dialogs are present, the
- /// background blur effect is also removed.
- ///
- private void TryPopModal() {
- if (UIView.HasModalInput()) {
- UIView.PopModal();
- UIComponent component = UIView.GetModalComponent();
- if (component != null) {
- UIView.SetFocus(component);
- }
- }
-
- if (blurEffect_ != null && UIView.ModalInputCount() == 0) {
- blurEffect_.eventPositionChanged -= OnBlurEffectPositionChange;
- blurEffect_.eventZOrderChanged -= OnBlurEffectZOrderChange;
- ValueAnimator.Animate(
- "ModalEffect",
- val => blurEffect_.opacity = val,
- new AnimatedFloat(1f, 0f, 0.7f, EasingType.CubicEaseOut),
- () => blurEffect_.Hide());
- }
- }
- }
-}
diff --git a/TLM/TLM/Util/ModsCompatibilityChecker.cs b/TLM/TLM/Util/ModsCompatibilityChecker.cs
deleted file mode 100644
index 3d5ac0dc8..000000000
--- a/TLM/TLM/Util/ModsCompatibilityChecker.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-namespace TrafficManager.Util {
- using ColossalFramework.Plugins;
- using ColossalFramework.UI;
- using ColossalFramework;
- using CSUtil.Commons;
- using ICities;
- using static ColossalFramework.Plugins.PluginManager;
- using System.Collections.Generic;
- using System.IO;
- using System.Reflection;
- using System;
- using TrafficManager.State;
- using TrafficManager.UI;
- using UnityEngine;
-
- public class ModsCompatibilityChecker {
- // Game always uses ulong.MaxValue to depict local mods
- private const ulong LOCAL_MOD = ulong.MaxValue;
-
- // Used for LoadIncompatibleModsList()
- private const string RESOURCES_PREFIX = "TrafficManager.Resources.";
- private const string INCOMPATIBLE_MODS_FILE = "incompatible_mods.txt";
-
- // parsed contents of incompatible_mods.txt
- private readonly Dictionary knownIncompatibleMods;
-
- public ModsCompatibilityChecker() {
- knownIncompatibleMods = LoadListOfIncompatibleMods();
- }
-
- ///
- /// Initiates scan for incompatible mods. If any found, and the user has enabled the mod checker, it creates and initialises the modal dialog panel.
- ///
- public void PerformModCheck() {
- try {
- Dictionary detected = ScanForIncompatibleMods();
-
- if (detected.Count > 0) {
- IncompatibleModsPanel panel =
- UIView.GetAView().AddUIComponent(typeof(IncompatibleModsPanel)) as
- IncompatibleModsPanel;
- panel.IncompatibleMods = detected;
- panel.Initialize();
- UIView.PushModal(panel);
- UIView.SetFocus(panel);
- }
- }
- catch (Exception e) {
- Log.Info(
- "Something went wrong while checking incompatible mods - see main game log for details.");
- Debug.LogException(e);
- }
- }
-
- ///
- /// Iterates installed mods looking for known incompatibilities.
- ///
- /// A list of detected incompatible mods.
- /// Invalid folder path (contains invalid characters,
- /// is empty, or contains only white spaces).
- /// Path is too long (longer than the system-defined
- /// maximum length).
- private Dictionary ScanForIncompatibleMods() {
- Guid selfGuid = Assembly.GetExecutingAssembly().ManifestModule.ModuleVersionId;
-
- // check known incompatible mods? (incompatible_mods.txt)
- bool checkKnown = GlobalConfig.Instance.Main.ScanForKnownIncompatibleModsAtStartup;
-
- // only check enabled mods?
- bool filterToEnabled = GlobalConfig.Instance.Main.IgnoreDisabledMods;
-
- // batch all logging in to a single log message
- string logStr = $"TM:PE Incompatible Mod Checker ({checkKnown},{filterToEnabled}):\n\n";
-
- // list of installed incompatible mods
- var results = new Dictionary();
-
- // iterate plugins
- foreach (PluginInfo mod in Singleton.instance.GetPluginsInfo()) {
- if (!mod.isBuiltin && !mod.isCameraScript) {
- string strModName = GetModName(mod);
- ulong workshopID = mod.publishedFileID.AsUInt64;
- bool isLocal = workshopID == LOCAL_MOD;
-
- string strEnabled = mod.isEnabled ? "*" : " ";
- string strWorkshopId = isLocal ? "(local)" : workshopID.ToString();
- string strIncompatible = " ";
-
- if (knownIncompatibleMods.ContainsKey(workshopID)) {
- strIncompatible = "!";
- if (checkKnown && (!filterToEnabled || mod.isEnabled)) {
- Debug.Log("[TM:PE] Incompatible mod detected: " + strModName);
- results.Add(mod, strModName);
- }
- } else if (strModName.Contains("TM:PE") ||
- strModName.Contains("Traffic Manager")) {
- if (GetModGuid(mod) != selfGuid) {
- string strFolder = Path.GetFileName(mod.modPath);
- strIncompatible = "!";
- Debug.Log(
- "[TM:PE] Duplicate instance detected: " + strModName + " in " +
- strFolder);
- results.Add(mod, strModName + " /" + strFolder);
- }
- }
-
- logStr +=
- $"{strIncompatible} {strEnabled} {strWorkshopId.PadRight(12)} {strModName}\n";
- }
- }
-
- Log.Info(logStr);
- Log.Info("Scan complete: " + results.Count + " incompatible mod(s) found");
-
- return results;
- }
-
- ///
- /// Gets the name of the specified mod.
- /// It will return the if found, otherwise it will return
- /// (assembly name).
- ///
- /// The associated with the mod.
- /// The name of the specified plugin.
- private string GetModName(PluginInfo plugin) {
- return ((IUserMod)plugin.userModInstance).Name;
- }
-
- ///
- /// Gets the of a mod.
- ///
- /// The associated with the mod.
- /// The of the mod.
- private Guid GetModGuid(PluginInfo plugin) {
- return plugin.userModInstance.GetType().Assembly.ManifestModule.ModuleVersionId;
- }
-
- ///
- /// Loads and parses the incompatible_mods.txt resource, adds other workshop branches
- /// of TM:PE as applicable.
- ///
- /// A dictionary of mod names referenced by Steam Workshop ID.
- private Dictionary LoadListOfIncompatibleMods() {
- // list of known incompatible mods
- var results = new Dictionary();
-
- // load the file
- string[] lines;
- using (Stream st = Assembly.GetExecutingAssembly()
- .GetManifestResourceStream(RESOURCES_PREFIX + INCOMPATIBLE_MODS_FILE))
- {
- using (var sr = new StreamReader(st)) {
- lines = sr.ReadToEnd().Split(
- new[] { "\n", "\r\n" },
- StringSplitOptions.None);
- }
- }
-
- // parse the file
- foreach (string line in lines) {
- if (string.IsNullOrEmpty(line)) {
- continue;
- }
-
- string[] strings = line.Split(';');
- if (ulong.TryParse(strings[0], out ulong steamId)) {
- results.Add(steamId, strings[1]);
- }
- }
-
- Log.Info($"{RESOURCES_PREFIX}{INCOMPATIBLE_MODS_FILE} contains {results.Count} entries");
-
- return results;
- }
- }
-}
diff --git a/docs/Mod Integration.md b/docs/Mod Integration.md
index 7662246cb..4700221e9 100644
--- a/docs/Mod Integration.md
+++ b/docs/Mod Integration.md
@@ -15,3 +15,11 @@ if (TMPE_Version < new Version(11)) {
// Version 11.1.0 and above all have correct assembly version in form M.m.b.* (Major, minor, build, *)
}
```
+
+## Local mod flagged as incompatible
+
+TM:PE will flag local mods as critically incompatible if they contain any of the following strings in their mod name:
+
+* `TM:PE` (use `TMPE` instead)
+* `Traffic Manager`
+* `Traffic++`