Skip to content

Commit

Permalink
Merge branch 'feature-compatibility'
Browse files Browse the repository at this point in the history
  • Loading branch information
dymanoid committed Aug 21, 2018
2 parents bafa03f + 6543a08 commit 3d1decb
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 106 deletions.
147 changes: 59 additions & 88 deletions src/RealTime/Core/Compatibility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,144 +8,115 @@ namespace RealTime.Core
using System.Collections.Generic;
using System.Linq;
using ColossalFramework.Plugins;
using ColossalFramework.UI;
using ICities;
using RealTime.Localization;
using SkyTools.Localization;
using SkyTools.Tools;
using SkyTools.UI;

/// <summary>
/// An utility class for checking the compatibility with other installed mods.
/// </summary>
internal static class Compatibility
internal sealed class Compatibility
{
/// <summary>The Workshop ID of the 'CitizenLifecycleRebalance' mod.</summary>
public const ulong CitizenLifecycleRebalanceId = 654707599;
private static readonly ulong[] IncompatibleModIds =
{
605590542, // Rush Hour II
629713122, // Climate Control
702070768, // Export Electricity
649522495, // District Service Limit
1181352643, // District Service Limit 3.0
};

private const string UIInfoPanel = "InfoPanel";
private const string UIPanelTime = "PanelTime";
private readonly string modName;
private readonly ILocalizationProvider localizationProvider;
private readonly Dictionary<ulong, PluginManager.PluginInfo> activeMods;

private static readonly HashSet<ulong> IncompatibleModIds = new HashSet<ulong>
private Compatibility(string modName, ILocalizationProvider localizationProvider)
{
605590542, 629713122, 702070768, 649522495, 1181352643,
};
this.modName = modName;
this.localizationProvider = localizationProvider;
activeMods = new Dictionary<ulong, PluginManager.PluginInfo>();
}

/// <summary>Checks for any enabled incompatible mods and notifies the player when any found.</summary>
/// <summary>Initializes a new instance of the <see cref="Compatibility"/> class.</summary>
/// <param name="modName">The name of the current mod.</param>
/// <param name="localizationProvider">The localization provider to use for translation.</param>
/// <param name="additionalInfo">Additional information to be added to the pop up or message box.</param>
/// <returns><c>true</c> if the check was successful and no messages were shown; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="modName"/> is null or an empty string.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="localizationProvider"/> is null.</exception>
public static bool Check(string modName, ILocalizationProvider localizationProvider, string additionalInfo = null)
/// <returns>A new and initialized instance of the <see cref="Compatibility"/> class.</returns>
public static Compatibility Create(string modName, ILocalizationProvider localizationProvider)
{
if (string.IsNullOrEmpty(modName))
{
throw new ArgumentException("The mod name cannot be null or an empty string", nameof(modName));
}

if (localizationProvider == null)
{
throw new ArgumentNullException(nameof(localizationProvider));
}
var result = new Compatibility(modName, localizationProvider ?? throw new ArgumentNullException(nameof(localizationProvider)));
result.Initialize();
return result;
}

/// <summary>Checks for enabled incompatible mods and prepares a notification message text if any found.</summary>
/// <param name="message">The translated message text about incompatible mods. If none found, <c>null</c>.</param>
/// <returns><c>true</c> if there are any active incompatible mod detected; otherwise, <c>false</c>.</returns>
public bool AreAnyIncompatibleModsActive(out string message)
{
List<string> incompatibleMods = GetIncompatibleModNames();
if (incompatibleMods.Count == 0)
{
return true;
message = null;
return false;
}

string separator = Environment.NewLine + " - ";
string caption = modName + " - " + localizationProvider.Translate(TranslationKeys.Warning);
string text = localizationProvider.Translate(TranslationKeys.IncompatibleModsFoundMessage)
message = localizationProvider.Translate(TranslationKeys.IncompatibleModsFoundMessage)
+ Environment.NewLine + separator
+ string.Join(separator, incompatibleMods.ToArray())
+ Environment.NewLine + Environment.NewLine
+ additionalInfo;

Notify(caption, text);
return false;
}
+ Environment.NewLine + Environment.NewLine;

/// <summary>Notifies the user either with a pop up or with a message box.</summary>
/// <param name="caption">The caption of the pop up or message box.</param>
/// <param name="text">The notification text.</param>
/// <exception cref="ArgumentException">Thrown when any argument is null or an empty string.</exception>
public static void Notify(string caption, string text)
{
if (string.IsNullOrEmpty(caption))
{
throw new ArgumentException("The caption cannot be null or an empty string", nameof(caption));
}

if (string.IsNullOrEmpty(text))
{
throw new ArgumentException("The text cannot be null or an empty string.", nameof(text));
}

if (!NotifyWithPopup(caption, text))
{
NotifyWithDialog(caption, text);
}
return true;
}

/// <summary>
/// Determines whether a mod with specified Workshop ID is currently installed and enabled.
/// Determines whether a mod with any of the specified Workshop IDs is currently installed and enabled.
/// </summary>
/// <param name="modId">The mod ID to check.</param>
/// <returns><c>true</c> if a mod with specified Workshop ID is currently installed and enabled; otherwise, <c>false</c>.</returns>
public static bool IsModActive(ulong modId)
{
return PluginManager.instance.GetPluginsInfo().Any(m => m.isEnabled && m.publishedFileID.AsUInt64 == modId);
}

private static void NotifyWithDialog(string caption, string text)
/// <param name="furtherModIds">Further mod IDs to check.</param>
/// <returns><c>true</c> if a mod with any of the specified Workshop ID is currently installed and enabled;
/// otherwise, <c>false</c>.</returns>
public bool IsAnyModActive(ulong modId, params ulong[] furtherModIds)
{
MessageBox.Show(caption, text);
return activeMods.ContainsKey(modId) || furtherModIds?.Any(activeMods.ContainsKey) == true;
}

private static bool NotifyWithPopup(string caption, string text)
private List<string> GetIncompatibleModNames()
{
UIPanel infoPanel = UIView.Find<UIPanel>(UIInfoPanel);
if (infoPanel == null)
{
Log.Warning("No UIPanel found: " + UIInfoPanel);
return false;
}

UIPanel panelTime = infoPanel.Find<UIPanel>(UIPanelTime);
if (panelTime == null)
var result = new List<string>();
foreach (ulong modId in IncompatibleModIds)
{
Log.Warning("No UIPanel found: " + UIPanelTime);
return false;
try
{
if (activeMods.TryGetValue(modId, out PluginManager.PluginInfo mod))
{
result.Add((mod.userModInstance as IUserMod)?.Name ?? mod.publishedFileID.AsUInt64.ToString());
}
}
catch (Exception ex)
{
Log.Warning($"The 'Real Time' mod wanted to check compatibility but failed, error message: {ex}");
}
}

Popup.Show(panelTime, caption, text);
return true;
return result;
}

private static List<string> GetIncompatibleModNames()
private void Initialize()
{
var result = new List<string>();

IEnumerable<PluginManager.PluginInfo> incompatibleMods = PluginManager.instance.GetPluginsInfo()
.Where(m => m.isEnabled && IncompatibleModIds.Contains(m.publishedFileID.AsUInt64));

try
activeMods.Clear();
foreach (PluginManager.PluginInfo plugin in PluginManager.instance.GetPluginsInfo().Where(m => m.isEnabled))
{
foreach (PluginManager.PluginInfo mod in incompatibleMods)
{
var userMod = mod.userModInstance as IUserMod;
result.Add(userMod == null ? mod.publishedFileID.AsUInt64.ToString() : userMod.Name);
}
}
catch (Exception ex)
{
Log.Warning($"The 'Real Time' mod wanted to check compatibility but failed, error message: {ex}");
activeMods[plugin.publishedFileID.AsUInt64] = plugin;
}

return result;
}
}
}
30 changes: 30 additions & 0 deletions src/RealTime/Core/ModIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// <copyright file="ModIds.cs" company="dymanoid">
// Copyright (c) dymanoid. All rights reserved.
// </copyright>

namespace RealTime.Core
{
/// <summary>
/// A class containing various mods' Workshop IDs.
/// </summary>
internal static class ModIds
{
/// <summary>The Workshop ID of the 'Citizen Lifecycle Rebalance' mod.</summary>
public const ulong CitizenLifecycleRebalance = 654707599ul;

/// <summary>The Workshop ID of the 'Ploppable RICO' mod.</summary>
public const ulong PloppableRico = 586012417ul;

/// <summary>The Workshop ID of the 'Ploppable RICO - High Density Fix' mod.</summary>
public const ulong PloppableRicoHighDensityFix = 1204126182ul;

/// <summary>The Workshop ID of the 'Plop The Growables' mod.</summary>
public const ulong PlopTheGrowables = 924884948ul;

/// <summary>The Workshop ID of the 'Building Themes' mod.</summary>
public const ulong BuildingThemes = 466158459ul;

/// <summary>The Workshop ID of the 'Force Level Up' mod.</summary>
public const ulong ForceLevelUp = 523818382ul;
}
}
35 changes: 28 additions & 7 deletions src/RealTime/Core/RealTimeCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,25 @@ private RealTimeCore(
/// Runs the mod by activating its parts.
/// </summary>
///
/// <exception cref="ArgumentNullException">Thrown when <paramref name="configProvider"/> or <paramref name="localizationProvider"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="configProvider"/> or <paramref name="localizationProvider"/>
/// or <paramref name="compatibility"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="rootPath"/> is null or an empty string.</exception>
///
/// <param name="configProvider">The configuration provider that provides the mod's configuration.</param>
/// <param name="rootPath">The path to the mod's assembly. Additional files are stored here too.</param>
/// <param name="localizationProvider">The <see cref="ILocalizationProvider"/> to use for text translation.</param>
/// <param name="setDefaultTime"><c>true</c> to initialize the game time to a default value (real world date and city wake up hour);
/// <c>false</c> to leave the game time unchanged.</param>
/// <param name="compatibility">The compatibility checker object.</param>
///
/// <returns>A <see cref="RealTimeCore"/> instance that can be used to stop the mod.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "This is the entry point and needs to instantiate all parts")]
public static RealTimeCore Run(
ConfigurationProvider<RealTimeConfig> configProvider,
string rootPath,
ILocalizationProvider localizationProvider,
bool setDefaultTime)
bool setDefaultTime,
Compatibility compatibility)
{
if (configProvider == null)
{
Expand All @@ -93,7 +96,12 @@ public static RealTimeCore Run(
throw new ArgumentNullException(nameof(localizationProvider));
}

List<IPatch> patches = GetMethodPatches();
if (compatibility == null)
{
throw new ArgumentNullException(nameof(compatibility));
}

List<IPatch> patches = GetMethodPatches(compatibility);
var patcher = new MethodPatcher(HarmonyId, patches);

HashSet<IPatch> appliedPatches = patcher.Apply();
Expand Down Expand Up @@ -277,7 +285,7 @@ public void Translate(ILocalizationProvider localizationProvider)
UIGraphPatches.Translate(localizationProvider.CurrentCulture);
}

private static List<IPatch> GetMethodPatches()
private static List<IPatch> GetMethodPatches(Compatibility compatibility)
{
var patches = new List<IPatch>
{
Expand All @@ -286,8 +294,6 @@ private static List<IPatch> GetMethodPatches()
BuildingAIPatches.CommercialSimulation,
BuildingAIPatches.GetColor,
BuildingAIPatches.CalculateUnspawnPosition,
BuildingAIPatches.GetUpgradeInfo,
BuildingAIPatches.CreateBuilding,
BuildingAIPatches.ProduceGoods,
ResidentAIPatch.Location,
ResidentAIPatch.ArriveAtTarget,
Expand All @@ -304,7 +310,7 @@ private static List<IPatch> GetMethodPatches()
OutsideConnectionAIPatch.DummyTrafficProbability,
};

if (Compatibility.IsModActive(Compatibility.CitizenLifecycleRebalanceId))
if (compatibility.IsAnyModActive(ModIds.CitizenLifecycleRebalance))
{
Log.Info("The 'Real Time' mod will not change the citizens aging because the 'Citizen Lifecycle Rebalance' mod is active.");
}
Expand All @@ -314,6 +320,21 @@ private static List<IPatch> GetMethodPatches()
patches.Add(ResidentAIPatch.CanMakeBabies);
}

if (compatibility.IsAnyModActive(
ModIds.BuildingThemes,
ModIds.ForceLevelUp,
ModIds.PloppableRico,
ModIds.PloppableRicoHighDensityFix,
ModIds.PlopTheGrowables))
{
Log.Info("The 'Real Time' mod will not change the building construction and upgrading behavior because some building mod is active.");
}
else
{
patches.Add(BuildingAIPatches.GetUpgradeInfo);
patches.Add(BuildingAIPatches.CreateBuilding);
}

patches.AddRange(TimeControlCompatibility.GetCompatibilityPatches());

return patches;
Expand Down
23 changes: 12 additions & 11 deletions src/RealTime/Core/RealTimeMod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,10 @@ public override void OnLevelLoaded(LoadMode mode)
Log.Info($"The 'Real Time' mod starts, game mode {mode}.");
core?.Stop();

var compatibility = Compatibility.Create(Name, localizationProvider);

bool isNewGame = mode == LoadMode.NewGame || mode == LoadMode.NewGameFromScenario;
core = RealTimeCore.Run(configProvider, modPath, localizationProvider, isNewGame);
core = RealTimeCore.Run(configProvider, modPath, localizationProvider, isNewGame, compatibility);
if (core == null)
{
Log.Warning("Showing a warning message to user because the mod isn't working");
Expand All @@ -150,7 +152,7 @@ public override void OnLevelLoaded(LoadMode mode)
}
else
{
CheckCompatibility();
CheckCompatibility(compatibility);
}
}

Expand Down Expand Up @@ -183,26 +185,25 @@ private static string GetModPath()
return pluginInfo?.modPath;
}

private void CheckCompatibility()
private void CheckCompatibility(Compatibility compatibility)
{
if (core == null)
{
return;
}

string restrictedText = core.IsRestrictedMode
? localizationProvider.Translate(TranslationKeys.RestrictedMode)
: null;
string message = null;
bool incompatibilitiesDetected = configProvider.Configuration.ShowIncompatibilityNotifications
&& compatibility.AreAnyIncompatibleModsActive(out message);

if (configProvider.Configuration.ShowIncompatibilityNotifications
&& !Compatibility.Check(Name, localizationProvider, restrictedText))
if (core.IsRestrictedMode)
{
return;
message += localizationProvider.Translate(TranslationKeys.RestrictedMode);
}

if (core.IsRestrictedMode)
if (incompatibilitiesDetected || core.IsRestrictedMode)
{
Compatibility.Notify(Name + " - " + localizationProvider.Translate(TranslationKeys.Warning), restrictedText);
Notification.Notify(Name + " - " + localizationProvider.Translate(TranslationKeys.Warning), message);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/RealTime/RealTime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Config\VirtualCitizensLevel.cs" />
<Compile Include="Core\ModIds.cs" />
<Compile Include="Core\RealTimeBenchmark.cs" />
<Compile Include="Core\Compatibility.cs" />
<Compile Include="Core\RealTimeStorage.cs" />
Expand Down Expand Up @@ -159,6 +160,7 @@
<Compile Include="UI\ConfigUI.cs" />
<Compile Include="UI\CustomVehicleInfoPanel.cs" />
<Compile Include="UI\DateTooltipBehavior.cs" />
<Compile Include="UI\Notification.cs" />
<Compile Include="UI\RealTimeInfoPanelBase.cs" />
<Compile Include="UI\RealTimeUIDateTimeWrapper.cs" />
<Compile Include="UI\CustomTimeBar.cs" />
Expand Down
Loading

0 comments on commit 3d1decb

Please sign in to comment.