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++`