diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89caee7765..7764f2f7f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file.
- [CLI] Correctly print cmdline errors with braces (#3880 by: HebaruSan; reviewed: techman83)
- [Multiple] Caching and changeset fixes (#3881 by: HebaruSan; reviewed: techman83)
- [GUI] Mod list fixes and improvements (#3883 by: HebaruSan; reviewed: techman83)
+- [Multiple] Multi-game labels (#3885 by: HebaruSan; reviewed: techman83)
### Internal
diff --git a/ConsoleUI/ExitScreen.cs b/ConsoleUI/ExitScreen.cs
index 2dead81959..86562f89d1 100644
--- a/ConsoleUI/ExitScreen.cs
+++ b/ConsoleUI/ExitScreen.cs
@@ -43,12 +43,12 @@ private void Draw(ConsoleTheme theme)
}
else
{
- Console.ResetColor();
+ Console.ResetColor();
}
Console.Clear();
// Specially formatted snippets
- var ckanPiece = new FancyLinePiece("CKAN", theme.ExitInnerBg, theme.ExitHighlightFg);
+ var ckanPiece = new FancyLinePiece(Meta.GetProductName(), theme.ExitInnerBg, theme.ExitHighlightFg);
var ckanVersionPiece = new FancyLinePiece($"CKAN {Meta.GetVersion()}", theme.ExitInnerBg, theme.ExitHighlightFg);
var releaseLinkPiece = new FancyLinePiece("https://github.com/KSP-CKAN/CKAN/releases/latest", theme.ExitInnerBg, theme.ExitLinkFg);
var issuesLinkPiece = new FancyLinePiece("https://github.com/KSP-CKAN/CKAN/issues", theme.ExitInnerBg, theme.ExitLinkFg);
diff --git a/Core/CKANPathUtils.cs b/Core/CKANPathUtils.cs
index 193b4aa65e..d0835a6b3e 100644
--- a/Core/CKANPathUtils.cs
+++ b/Core/CKANPathUtils.cs
@@ -9,6 +9,13 @@ namespace CKAN
{
public static class CKANPathUtils
{
+ ///
+ /// Path to save CKAN data shared across all game instances
+ ///
+ public static readonly string AppDataPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ Meta.GetProductName());
+
private static readonly ILog log = LogManager.GetLogger(typeof(CKANPathUtils));
///
diff --git a/Core/CompatibleGameVersions.cs b/Core/CompatibleGameVersions.cs
index c54e0a92a7..ae5744dbb4 100644
--- a/Core/CompatibleGameVersions.cs
+++ b/Core/CompatibleGameVersions.cs
@@ -14,15 +14,10 @@ class CompatibleGameVersions
public class CompatibleGameVersionsConverter : JsonPropertyNamesChangedConverter
{
protected override Dictionary mapping
- {
- get
+ => new Dictionary
{
- return new Dictionary
- {
- { "VersionOfKspWhenWritten", "GameVersionWhenWritten" },
- { "CompatibleKspVersions", "Versions" }
- };
- }
- }
+ { "VersionOfKspWhenWritten", "GameVersionWhenWritten" },
+ { "CompatibleKspVersions", "Versions" }
+ };
}
}
diff --git a/Core/Configuration/JsonConfiguration.cs b/Core/Configuration/JsonConfiguration.cs
index 7a78debf98..fae03c1a46 100644
--- a/Core/Configuration/JsonConfiguration.cs
+++ b/Core/Configuration/JsonConfiguration.cs
@@ -51,17 +51,10 @@ private class GameInstanceEntry
// CKAN_CONFIG_FILE environment variable.
public static readonly string defaultConfigFile =
Environment.GetEnvironmentVariable("CKAN_CONFIG_FILE")
- ?? Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "CKAN",
- "config.json"
- );
-
- public static readonly string DefaultDownloadCacheDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "CKAN",
- "downloads"
- );
+ ?? Path.Combine(CKANPathUtils.AppDataPath, "config.json");
+
+ public static readonly string DefaultDownloadCacheDir =
+ Path.Combine(CKANPathUtils.AppDataPath, "downloads");
// The actual config file state, and its location on the disk (we allow
// the location to be changed for unit tests). Note that these are static
diff --git a/Core/Configuration/Win32RegistryConfiguration.cs b/Core/Configuration/Win32RegistryConfiguration.cs
index 2978020f59..6f35891291 100644
--- a/Core/Configuration/Win32RegistryConfiguration.cs
+++ b/Core/Configuration/Win32RegistryConfiguration.cs
@@ -21,11 +21,8 @@ public class Win32RegistryConfiguration : IConfiguration
private const string authTokenKey = CKAN_KEY + @"\AuthTokens";
private static readonly string authTokenKeyNoPrefix = StripPrefixKey(authTokenKey);
- private static readonly string defaultDownloadCacheDir = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "CKAN",
- "downloads"
- );
+ private static readonly string defaultDownloadCacheDir =
+ Path.Combine(CKANPathUtils.AppDataPath, "downloads");
public string DownloadCacheDir
{
diff --git a/Core/Converters/JsonPropertyNamesChangedConverter.cs b/Core/Converters/JsonPropertyNamesChangedConverter.cs
index ec1a0c5240..a9e125567b 100644
--- a/Core/Converters/JsonPropertyNamesChangedConverter.cs
+++ b/Core/Converters/JsonPropertyNamesChangedConverter.cs
@@ -2,28 +2,53 @@
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
+
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CKAN
{
+ ///
+ /// Base class for a class-level converter that transfers values from
+ /// the old name for a property to its new name.
+ /// Inherit, then override the mapping property to specify the renamings.
+ ///
public abstract class JsonPropertyNamesChangedConverter : JsonConverter
{
+ ///
+ /// We don't want to make any changes during serialization
+ ///
public override bool CanWrite => false;
+
+ ///
+ /// We don't want to make any changes during serialization
+ ///
+ /// The object writing JSON to disk
+ /// A value to be written for this class
+ /// Generates output objects from tokens
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
- public override bool CanConvert(Type objectType)
- {
- return objectType.GetTypeInfo().IsClass;
- }
+ ///
+ /// We only want to convert classes, not properties
+ ///
+ /// Type where this class been used as a JsonConverter
+ /// true if it's a class, false otherwise
+ public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().IsClass;
+ ///
+ /// Parse JSON to an object, renaming properties according to the mapping property
+ ///
+ /// Object that provides tokens to be translated
+ /// The output type to be populated
+ /// Not used
+ /// Generates output objects from tokens
+ /// Class object populated according to the renaming scheme
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
object instance = Activator.CreateInstance(objectType);
- var props = objectType.GetTypeInfo().DeclaredProperties.ToList();
JObject jo = JObject.Load(reader);
var changes = mapping;
foreach (JProperty jp in jo.Properties())
@@ -33,15 +58,48 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
{
name = jp.Name;
}
- PropertyInfo prop = props.FirstOrDefault(pi => pi.CanWrite && (
- pi.GetCustomAttribute()?.PropertyName == name
- || pi.Name == name));
- prop?.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
+ PropertyInfo prop = objectType.GetTypeInfo().DeclaredProperties.FirstOrDefault(pi =>
+ pi.CanWrite
+ && (pi.GetCustomAttribute()?.PropertyName ?? pi.Name) == name);
+ if (prop != null)
+ {
+ prop.SetValue(instance,
+ GetValue(prop.GetCustomAttribute(),
+ jp.Value, prop.PropertyType, serializer));
+ }
+ else
+ {
+ // No property, maybe there's a field
+ FieldInfo field = objectType.GetTypeInfo().DeclaredFields.FirstOrDefault(fi =>
+ (fi.GetCustomAttribute()?.PropertyName ?? fi.Name) == name);
+ if (field != null)
+ {
+ field.SetValue(instance,
+ GetValue(field.GetCustomAttribute(),
+ jp.Value, field.FieldType, serializer));
+ }
+ }
}
return instance;
}
- // This is what you need to override in the child class
+ private static object GetValue(JsonConverterAttribute attrib,
+ JToken value, Type outputType, JsonSerializer serializer)
+ => attrib != null ? ApplyConverter((JsonConverter)Activator.CreateInstance(attrib.ConverterType,
+ attrib.ConverterParameters),
+ value, outputType, serializer)
+ : value.ToObject(outputType, serializer);
+
+ private static object ApplyConverter(JsonConverter converter,
+ JToken value, Type outputType, JsonSerializer serializer)
+ => converter.CanRead ? converter.ReadJson(new JTokenReader(value),
+ outputType, null, serializer)
+ : value.ToObject(outputType, serializer);
+
+ ///
+ /// This is what you need to override in your child class
+ ///
+ /// Mapping from old names to new names
protected abstract Dictionary mapping
{
get;
diff --git a/Core/Converters/JsonSingleOrArrayConverter.cs b/Core/Converters/JsonSingleOrArrayConverter.cs
index 5db5b29db2..7f760ed1ee 100644
--- a/Core/Converters/JsonSingleOrArrayConverter.cs
+++ b/Core/Converters/JsonSingleOrArrayConverter.cs
@@ -41,9 +41,6 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
///
/// false
///
- public override bool CanConvert(Type object_type)
- {
- return false;
- }
+ public override bool CanConvert(Type object_type) => false;
}
}
diff --git a/Core/Converters/JsonToGamesDictionaryConverter.cs b/Core/Converters/JsonToGamesDictionaryConverter.cs
new file mode 100644
index 0000000000..c348a3c6e7
--- /dev/null
+++ b/Core/Converters/JsonToGamesDictionaryConverter.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Linq;
+using System.Collections;
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace CKAN
+{
+ ///
+ /// A property converter for making an old property game-specific.
+ /// Turns a String or Array value:
+ ///
+ /// "myProperty": "a value",
+ /// "myOtherProperty": [ "another value" ],
+ ///
+ /// into a Dictionary with the game names as keys and the original
+ /// value as each value:
+ ///
+ /// "myProperty": {
+ /// "KSP": "a value",
+ /// "KSP2": "a value"
+ /// },
+ /// "myOtherProperty": {
+ /// "KSP": [ "another value" ],
+ /// "KSP2": [ "another value" ]
+ /// },
+ ///
+ /// NOTE: Do NOT use with Object values because they can't
+ /// be distinguished from an already converted value, and will
+ /// just be deserialized as-is into your Dictionary!
+ ///
+ /// If the value is an empty array:
+ ///
+ /// "myProperty": [],
+ ///
+ /// the Dictionary is left empty rather than creating multiple keys
+ /// with empty values:
+ ///
+ /// "myProperty": {},
+ ///
+ public class JsonToGamesDictionaryConverter : JsonConverter
+ {
+ ///
+ /// Turn a tree of JSON tokens into a dictionary
+ ///
+ /// Object that provides tokens to be translated
+ /// The output type to be populated
+ /// Not used
+ /// Generates output objects from tokens
+ /// Dictionary of type matching the property where this converter was used, containing game-specific keys and values
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var token = JToken.Load(reader);
+ if (token.Type == JTokenType.Object)
+ {
+ return token.ToObject(objectType);
+ }
+ var valueType = objectType.GetGenericArguments()[1];
+ var obj = (IDictionary)Activator.CreateInstance(objectType);
+ if (!IsTokenEmpty(token))
+ {
+ foreach (var gameName in GameInstanceManager.AllGameShortNames())
+ {
+ // Make a new copy of the value for each game
+ obj.Add(gameName, token.ToObject(valueType));
+ }
+ }
+ return obj;
+ }
+
+ ///
+ /// We don't want to make any changes during serialization
+ ///
+ public override bool CanWrite => false;
+
+ ///
+ /// We don't want to make any changes during serialization
+ ///
+ /// The object writing JSON to disk
+ /// A value to be written for this class
+ /// Generates output objects from tokens
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// We *only* want to be triggered for types that have explicitly
+ /// set an attribute in their class saying they can be converted.
+ /// By returning false here, we declare we're not interested in participating
+ /// in any other conversions.
+ ///
+ ///
+ /// false
+ ///
+ public override bool CanConvert(Type object_type) => false;
+
+ private static bool IsTokenEmpty(JToken token)
+ => token.Type == JTokenType.Null
+ || (token.Type == JTokenType.Array && !token.HasValues);
+ }
+}
diff --git a/Core/GameInstanceManager.cs b/Core/GameInstanceManager.cs
index c95b4b3f6e..67bf3a317a 100644
--- a/Core/GameInstanceManager.cs
+++ b/Core/GameInstanceManager.cs
@@ -612,9 +612,7 @@ public void Dispose()
}
public static bool IsGameInstanceDir(DirectoryInfo path)
- {
- return knownGames.Any(g => g.GameInFolder(path));
- }
+ => knownGames.Any(g => g.GameInFolder(path));
///
/// Tries to determine the game that is installed at the given path
@@ -643,8 +641,20 @@ public IGame DetermineGame(DirectoryInfo path, IUser user)
}
}
+ ///
+ /// Return a game object based on its short name
+ ///
+ /// The short name to find
+ /// A game object or null if none found
public static IGame GameByShortName(string shortName)
=> knownGames.FirstOrDefault(g => g.ShortName == shortName);
+ ///
+ /// Return the short names of all known games
+ ///
+ /// Sequence of short name strings
+ public static IEnumerable AllGameShortNames()
+ => knownGames.Select(g => g.ShortName);
+
}
}
diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs
index 6da7278ad1..37f634c6db 100644
--- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs
+++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs
@@ -33,9 +33,8 @@ public sealed class KspBuildMap : IKspBuildMap
// TODO: Need a way for the client to configure this
private static readonly Uri BuildMapUri =
new Uri("https://raw.githubusercontent.com/KSP-CKAN/CKAN-meta/master/builds.json");
- private static readonly string cachedBuildMapPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "CKAN", "builds-ksp.json");
+ private static readonly string cachedBuildMapPath =
+ Path.Combine(CKANPathUtils.AppDataPath, "builds-ksp.json");
private static readonly ILog Log = LogManager.GetLogger(typeof(KspBuildMap));
diff --git a/Core/Games/KerbalSpaceProgram2.cs b/Core/Games/KerbalSpaceProgram2.cs
index fe699f7423..af1ac89c4b 100644
--- a/Core/Games/KerbalSpaceProgram2.cs
+++ b/Core/Games/KerbalSpaceProgram2.cs
@@ -151,9 +151,8 @@ public string[] AdjustCommandLine(string[] args, GameVersion installedVersion)
private static readonly Uri BuildMapUri =
new Uri("https://raw.githubusercontent.com/KSP-CKAN/KSP2-CKAN-meta/main/builds.json");
- private static readonly string cachedBuildMapPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "CKAN", "builds-ksp2.json");
+ private static readonly string cachedBuildMapPath =
+ Path.Combine(CKANPathUtils.AppDataPath, "builds-ksp2.json");
private List versions = JsonConvert.DeserializeObject>(
File.Exists(cachedBuildMapPath)
diff --git a/Core/Meta.cs b/Core/Meta.cs
index 0c3246ca47..c4d8a620d8 100644
--- a/Core/Meta.cs
+++ b/Core/Meta.cs
@@ -1,17 +1,27 @@
using System;
+using System.Linq;
using System.Reflection;
namespace CKAN
{
public static class Meta
{
+ ///
+ /// Programmatically generate the string "CKAN" from the assembly info attributes,
+ /// so we don't have to embed that string in many places
+ ///
+ /// "CKAN"
+ public static string GetProductName()
+ => Assembly.GetExecutingAssembly()
+ .GetAssemblyAttribute()
+ .Product;
+
public static string GetVersion(VersionFormat format = VersionFormat.Normal)
{
- var version = ((AssemblyInformationalVersionAttribute)
- Assembly
- .GetExecutingAssembly()
- .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false)[0]
- ).InformationalVersion;
+ var version = Assembly
+ .GetExecutingAssembly()
+ .GetAssemblyAttribute()
+ .InformationalVersion;
var dashIndex = version.IndexOf('-');
var plusIndex = version.IndexOf('+');
@@ -38,5 +48,9 @@ public static string GetVersion(VersionFormat format = VersionFormat.Normal)
return "v" + version;
}
+
+ private static T GetAssemblyAttribute(this Assembly assembly)
+ => (T)assembly.GetCustomAttributes(typeof(T), false)
+ .First();
}
}
diff --git a/Core/Registry/Tags/ModuleTagList.cs b/Core/Registry/Tags/ModuleTagList.cs
index b4d0993e52..12ae01e5c4 100644
--- a/Core/Registry/Tags/ModuleTagList.cs
+++ b/Core/Registry/Tags/ModuleTagList.cs
@@ -17,11 +17,8 @@ public class ModuleTagList
[JsonProperty("hidden_tags")]
public HashSet HiddenTags = new HashSet();
- public static readonly string DefaultPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "CKAN",
- "tags.json"
- );
+ public static readonly string DefaultPath =
+ Path.Combine(CKANPathUtils.AppDataPath, "tags.json");
public void BuildTagIndexFor(AvailableModule am)
{
@@ -51,7 +48,7 @@ public void BuildTagIndexFor(AvailableModule am)
Untagged.Add(am.AllAvailable().First().identifier);
}
}
-
+
public static ModuleTagList Load(string path)
{
try
diff --git a/GUI/Controls/Changeset.cs b/GUI/Controls/Changeset.cs
index 0d1152cad1..0b1aca8148 100644
--- a/GUI/Controls/Changeset.cs
+++ b/GUI/Controls/Changeset.cs
@@ -86,7 +86,8 @@ private ListViewItem makeItem(ModChange change, Dictionary c
{
var descr = change.Description;
CkanModule m = change.Mod;
- ModuleLabel warnLbl = alertLabels?.FirstOrDefault(l => l.ModuleIdentifiers.Contains(m.identifier));
+ ModuleLabel warnLbl = alertLabels?.FirstOrDefault(l =>
+ l.ContainsModule(Main.Instance.CurrentInstance.game, m.identifier));
return new ListViewItem(new string[]
{
change.NameAndStatus,
diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs
index 99eb69d1e3..5830e48c29 100644
--- a/GUI/Controls/ManageMods.cs
+++ b/GUI/Controls/ManageMods.cs
@@ -166,6 +166,7 @@ private void ConflictsUpdated(Dictionary prevConflicts)
{
// Mark old conflicts as non-conflicted
// (rows that are _still_ conflicted will be marked as such in the next loop)
+ var inst = Main.Instance.CurrentInstance;
foreach (GUIMod guiMod in prevConflicts.Keys)
{
DataGridViewRow row = mainModList.full_list_of_mod_rows[guiMod.Identifier];
@@ -174,7 +175,7 @@ private void ConflictsUpdated(Dictionary prevConflicts)
{
cell.ToolTipText = null;
}
- mainModList.ReapplyLabels(guiMod, false, Main.Instance.CurrentInstance.Name);
+ mainModList.ReapplyLabels(guiMod, false, inst.Name, inst.game);
if (row.Visible)
{
ModGrid.InvalidateRow(row.Index);
@@ -247,7 +248,7 @@ private void FilterLabelsToolButton_DropDown_Opening(object sender, CancelEventA
foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(Main.Instance.CurrentInstance.Name))
{
FilterLabelsToolButton.DropDownItems.Add(new ToolStripMenuItem(
- $"{mlbl.Name} ({mlbl.ModuleIdentifiers.Count})",
+ $"{mlbl.Name} ({mlbl.ModuleCount(Main.Instance.CurrentInstance.game)})",
null, customFilterButton_Click
)
{
@@ -271,7 +272,7 @@ private void LabelsContextMenuStrip_Opening(object sender, CancelEventArgs e)
LabelsContextMenuStrip.Items.Add(
new ToolStripMenuItem(mlbl.Name, null, labelMenuItem_Click)
{
- Checked = mlbl.ModuleIdentifiers.Contains(module.Identifier),
+ Checked = mlbl.ContainsModule(Main.Instance.CurrentInstance.game, module.Identifier),
CheckOnClick = true,
Tag = mlbl,
}
@@ -289,18 +290,19 @@ private void labelMenuItem_Click(object sender, EventArgs e)
var module = SelectedModule;
if (item.Checked)
{
- mlbl.Add(module.Identifier);
+ mlbl.Add(Main.Instance.CurrentInstance.game, module.Identifier);
}
else
{
- mlbl.Remove(module.Identifier);
+ mlbl.Remove(Main.Instance.CurrentInstance.game, module.Identifier);
}
if (mlbl.HoldVersion)
{
UpdateAllToolButton.Enabled = mainModList.Modules.Any(mod =>
mod.HasUpdate && !Main.Instance.LabelsHeld(mod.Identifier));
}
- mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, Main.Instance.CurrentInstance.Name);
+ var inst = Main.Instance.CurrentInstance;
+ mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game);
mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath);
}
@@ -310,9 +312,10 @@ private void editLabelsToolStripMenuItem_Click(object sender, EventArgs e)
eld.ShowDialog(this);
eld.Dispose();
mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath);
+ var inst = Main.Instance.CurrentInstance;
foreach (GUIMod module in mainModList.Modules)
{
- mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, Main.Instance.CurrentInstance.Name);
+ mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game);
}
}
@@ -1129,7 +1132,8 @@ private void _UpdateFilters()
foreach (var row in rows)
{
var mod = ((GUIMod) row.Tag);
- row.Visible = mainModList.IsVisible(mod, Main.Instance.CurrentInstance.Name);
+ var inst = Main.Instance.CurrentInstance;
+ row.Visible = mainModList.IsVisible(mod, inst.Name, inst.game);
}
ApplyHeaderGlyphs();
@@ -1221,7 +1225,7 @@ private void _UpdateModsList(Dictionary old_modules = null)
Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListPopulatingList);
// Update our mod listing
- mainModList.ConstructModList(gui_mods, Main.Instance.CurrentInstance.Name, ChangeSet);
+ mainModList.ConstructModList(gui_mods, Main.Instance.CurrentInstance.Name, Main.Instance.CurrentInstance.game, ChangeSet);
// C# 7.0: Executes the task and discards it
_ = UpdateChangeSetAndConflicts(Main.Instance.CurrentInstance, registry);
diff --git a/GUI/Controls/ModInfo.cs b/GUI/Controls/ModInfo.cs
index 990e50d56f..d78c675581 100644
--- a/GUI/Controls/ModInfo.cs
+++ b/GUI/Controls/ModInfo.cs
@@ -156,7 +156,7 @@ private void UpdateTagsAndLabels(CkanModule mod)
}
}
var labels = ModuleLabels?.LabelsFor(manager.CurrentInstance.Name)
- .Where(l => l.ModuleIdentifiers.Contains(mod.identifier))
+ .Where(l => l.ContainsModule(Main.Instance.CurrentInstance.game, mod.identifier))
.OrderBy(l => l.Name);
if (labels != null)
{
diff --git a/GUI/Labels/ModuleLabel.cs b/GUI/Labels/ModuleLabel.cs
index 3392ff6d72..a49aa04e13 100644
--- a/GUI/Labels/ModuleLabel.cs
+++ b/GUI/Labels/ModuleLabel.cs
@@ -1,11 +1,19 @@
+using System;
+using System.Linq;
using System.Drawing;
using System.ComponentModel;
+using System.Collections;
using System.Collections.Generic;
+
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+using CKAN.Games;
namespace CKAN.GUI
{
[JsonObject(MemberSerialization.OptIn)]
+ [JsonConverter(typeof(ModuleIdentifiersRenamedConverter))]
public class ModuleLabel
{
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
@@ -41,8 +49,31 @@ public class ModuleLabel
[DefaultValue(false)]
public bool HoldVersion;
- [JsonProperty("module_identifiers", NullValueHandling = NullValueHandling.Ignore)]
- public HashSet ModuleIdentifiers = new HashSet();
+ [JsonProperty("module_identifiers_by_game", NullValueHandling = NullValueHandling.Ignore)]
+ [JsonConverter(typeof(JsonToGamesDictionaryConverter))]
+ private Dictionary> ModuleIdentifiers =
+ new Dictionary>();
+
+ ///
+ /// Return the number of modules associated with this label for a given game
+ ///
+ /// Game to check
+ /// Number of modules
+ public int ModuleCount(IGame game)
+ => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet identifiers)
+ ? identifiers.Count
+ : 0;
+
+ ///
+ /// Return whether a given identifier is associated with this label for a given game
+ ///
+ /// The game to check
+ /// The identifier to check
+ /// true if this label applies to this identifier, false otherwise
+ public bool ContainsModule(IGame game, string identifier)
+ => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet identifiers)
+ ? identifiers.Contains(identifier)
+ : false;
///
/// Check whether this label is active for a given game instance
@@ -52,26 +83,50 @@ public class ModuleLabel
/// True if active, false otherwise
///
public bool AppliesTo(string instanceName)
- {
- return InstanceName == null || InstanceName == instanceName;
- }
+ => InstanceName == null || InstanceName == instanceName;
///
/// Add a module to this label's group
///
/// The identifier of the module to add
- public void Add(string identifier)
+ public void Add(IGame game, string identifier)
{
- ModuleIdentifiers.Add(identifier);
+ if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet identifiers))
+ {
+ identifiers.Add(identifier);
+ }
+ else
+ {
+ ModuleIdentifiers.Add(game.ShortName, new HashSet {identifier});
+ }
}
///
/// Remove a module from this label's group
///
/// The identifier of the module to remove
- public void Remove(string identifier)
+ public void Remove(IGame game, string identifier)
{
- ModuleIdentifiers.Remove(identifier);
+ if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet identifiers))
+ {
+ identifiers.Remove(identifier);
+ if (identifiers.Count < 1)
+ {
+ ModuleIdentifiers.Remove(game.ShortName);
+ }
+ }
}
}
+
+ ///
+ /// Protect old clients from trying to load a file they can't parse
+ ///
+ public class ModuleIdentifiersRenamedConverter : JsonPropertyNamesChangedConverter
+ {
+ protected override Dictionary mapping
+ => new Dictionary
+ {
+ { "module_identifiers", "module_identifiers_by_game" }
+ };
+ }
}
diff --git a/GUI/Labels/ModuleLabelList.cs b/GUI/Labels/ModuleLabelList.cs
index f58e542c5c..52357efc88 100644
--- a/GUI/Labels/ModuleLabelList.cs
+++ b/GUI/Labels/ModuleLabelList.cs
@@ -4,6 +4,7 @@
using System.IO;
using System.Drawing;
using System.Runtime.Serialization;
+
using Newtonsoft.Json;
namespace CKAN.GUI
@@ -15,19 +16,13 @@ public class ModuleLabelList
public ModuleLabel[] Labels = new ModuleLabel[] {};
public IEnumerable LabelsFor(string instanceName)
- {
- return Labels.Where(l => l.AppliesTo(instanceName));
- }
+ => Labels.Where(l => l.AppliesTo(instanceName));
- public static readonly string DefaultPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "CKAN",
- "labels.json"
- );
+ public static readonly string DefaultPath =
+ Path.Combine(CKANPathUtils.AppDataPath, "labels.json");
public static ModuleLabelList GetDefaultLabels()
- {
- return new ModuleLabelList()
+ => new ModuleLabelList()
{
Labels = new ModuleLabel[]
{
@@ -50,7 +45,6 @@ public static ModuleLabelList GetDefaultLabels()
}
}
};
- }
public static ModuleLabelList Load(string path)
{
diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs
index 86fbf4ad27..87c20b59a9 100644
--- a/GUI/Main/Main.cs
+++ b/GUI/Main/Main.cs
@@ -113,7 +113,7 @@ public Main(string[] cmdlineArgs, GameInstanceManager mgr)
{
Interval = 2
};
- timer.Tick += (sender, e) => Thread.Yield();;
+ timer.Tick += (sender, e) => Thread.Yield();
timer.Start();
}
diff --git a/GUI/Main/MainLabels.cs b/GUI/Main/MainLabels.cs
index 89f194a126..4266ebb522 100644
--- a/GUI/Main/MainLabels.cs
+++ b/GUI/Main/MainLabels.cs
@@ -20,7 +20,7 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable mods)
var toNotif = mods
.Where(m =>
notifLabs.Any(l =>
- l.ModuleIdentifiers.Contains(m.Identifier)))
+ l.ContainsModule(CurrentInstance.game, m.Identifier)))
.Select(m => m.Name)
.Memoize();
if (toNotif.Any())
@@ -39,9 +39,9 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable mods)
{
foreach (ModuleLabel l in ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)
.Where(l => l.RemoveOnChange
- && l.ModuleIdentifiers.Contains(mod.Identifier)))
+ && l.ContainsModule(CurrentInstance.game, mod.Identifier)))
{
- l.Remove(mod.Identifier);
+ l.Remove(CurrentInstance.game, mod.Identifier);
}
}
});
@@ -50,17 +50,15 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable mods)
private void LabelsAfterInstall(CkanModule mod)
{
foreach (ModuleLabel l in ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)
- .Where(l => l.RemoveOnInstall && l.ModuleIdentifiers.Contains(mod.identifier)))
+ .Where(l => l.RemoveOnInstall && l.ContainsModule(CurrentInstance.game, mod.identifier)))
{
- l.Remove(mod.identifier);
+ l.Remove(CurrentInstance.game, mod.identifier);
}
}
public bool LabelsHeld(string identifier)
- {
- return ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)
- .Any(l => l.HoldVersion && l.ModuleIdentifiers.Contains(identifier));
- }
+ => ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)
+ .Any(l => l.HoldVersion && l.ContainsModule(CurrentInstance.game, identifier));
#endregion
}
diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs
index 469db2a9d8..a08e8c5d52 100644
--- a/GUI/Model/ModList.cs
+++ b/GUI/Model/ModList.cs
@@ -12,6 +12,7 @@
using CKAN.Versioning;
using CKAN.Extensions;
+using CKAN.Games;
namespace CKAN.GUI
{
@@ -230,9 +231,9 @@ private IEnumerable InstalledAfterChanges(
false)));
}
- public bool IsVisible(GUIMod mod, string instanceName)
+ public bool IsVisible(GUIMod mod, string instanceName, IGame game)
=> (activeSearches?.Any(s => s?.Matches(mod) ?? true) ?? true)
- && !HiddenByTagsOrLabels(mod, instanceName);
+ && !HiddenByTagsOrLabels(mod, instanceName, game);
private bool TagInSearches(ModuleTag tag)
=> activeSearches?.Any(s => s?.TagNames.Contains(tag.Name) ?? false) ?? false;
@@ -240,11 +241,11 @@ private bool TagInSearches(ModuleTag tag)
private bool LabelInSearches(ModuleLabel label)
=> activeSearches?.Any(s => s?.Labels.Contains(label) ?? false) ?? false;
- private bool HiddenByTagsOrLabels(GUIMod m, string instanceName)
+ private bool HiddenByTagsOrLabels(GUIMod m, string instanceName, IGame game)
// "Hide" labels apply to all non-custom filters
=> (ModuleLabels?.LabelsFor(instanceName)
.Where(l => !LabelInSearches(l) && l.Hide)
- .Any(l => l.ModuleIdentifiers.Contains(m.Identifier))
+ .Any(l => l.ContainsModule(game, m.Identifier))
?? false)
|| (ModuleTags?.Tags?.Values
.Where(t => !TagInSearches(t) && t.Visible == false)
@@ -265,23 +266,23 @@ public int CountModsByFilter(GUIModFilter filter)
/// Changes the user has made
/// The mod list
public IEnumerable ConstructModList(
- IEnumerable modules, string instanceName, IEnumerable mc = null)
+ IEnumerable modules, string instanceName, IGame game, IEnumerable mc = null)
{
Modules = new ReadOnlyCollection(modules.ToList());
var changes = mc?.ToList();
full_list_of_mod_rows = Modules.ToDictionary(
gm => gm.Identifier,
- gm => MakeRow(gm, changes, instanceName));
+ gm => MakeRow(gm, changes, instanceName, game));
HasAnyInstalled = Modules.Any(m => m.IsInstalled);
return full_list_of_mod_rows.Values;
}
- private DataGridViewRow MakeRow(GUIMod mod, List changes, string instanceName)
+ private DataGridViewRow MakeRow(GUIMod mod, List changes, string instanceName, IGame game)
{
DataGridViewRow item = new DataGridViewRow() {Tag = mod};
Color? myColor = ModuleLabels.LabelsFor(instanceName)
- .FirstOrDefault(l => l.ModuleIdentifiers.Contains(mod.Identifier))
+ .FirstOrDefault(l => l.ContainsModule(game, mod.Identifier))
?.Color;
if (myColor.HasValue)
{
@@ -380,7 +381,7 @@ public Color GetRowBackground(GUIMod mod, bool conflicted, string instanceName)
=> conflicted ? Color.LightCoral
: full_list_of_mod_rows.ContainsKey(mod.Identifier)
? ModuleLabels.LabelsFor(instanceName)
- .FirstOrDefault(l => l.ModuleIdentifiers.Contains(mod.Identifier))
+ .FirstOrDefault(l => l.ContainsModule(Main.Instance.CurrentInstance.game, mod.Identifier))
?.Color
?? Color.Empty
: Color.Empty;
@@ -390,13 +391,13 @@ public Color GetRowBackground(GUIMod mod, bool conflicted, string instanceName)
/// after it has been added to or removed from a label group
///
/// The mod that needs an update
- public void ReapplyLabels(GUIMod mod, bool conflicted, string instanceName)
+ public void ReapplyLabels(GUIMod mod, bool conflicted, string instanceName, IGame game)
{
DataGridViewRow row;
if (full_list_of_mod_rows.TryGetValue(mod.Identifier, out row))
{
row.DefaultCellStyle.BackColor = GetRowBackground(mod, conflicted, instanceName);
- row.Visible = IsVisible(mod, instanceName);
+ row.Visible = IsVisible(mod, instanceName, game);
}
}
diff --git a/GUI/Model/ModSearch.cs b/GUI/Model/ModSearch.cs
index bfb63532a6..3b9133bc1f 100644
--- a/GUI/Model/ModSearch.cs
+++ b/GUI/Model/ModSearch.cs
@@ -546,7 +546,8 @@ private bool MatchesTags(GUIMod mod)
private bool MatchesLabels(GUIMod mod)
// Every label in Labels must contain this mod
- => Labels.Count < 1 || Labels.All(lb => lb.ModuleIdentifiers.Contains(mod.Identifier));
+ => Labels.Count < 1 || Labels.All(lb =>
+ lb.ContainsModule(Main.Instance.CurrentInstance.game, mod.Identifier));
private bool MatchesCompatible(GUIMod mod)
=> !Compatible.HasValue || Compatible.Value == !mod.IsIncompatible;
diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs
index 3b092aec6b..ec9b6ac085 100644
--- a/Tests/GUI/Model/ModList.cs
+++ b/Tests/GUI/Model/ModList.cs
@@ -45,7 +45,8 @@ public void IsVisible_WithAllAndNoNameFilter_ReturnsTrueForCompatible()
var item = new ModList();
Assert.That(item.IsVisible(
new GUIMod(ckan_mod, registry, manager.CurrentInstance.VersionCriteria()),
- manager.CurrentInstance.Name
+ manager.CurrentInstance.Name,
+ manager.CurrentInstance.game
));
manager.Dispose();
@@ -89,7 +90,8 @@ public void ConstructModList_NumberOfRows_IsEqualToNumberOfMods()
new GUIMod(TestData.FireSpitterModule(), registry, manager.CurrentInstance.VersionCriteria()),
new GUIMod(TestData.kOS_014_module(), registry, manager.CurrentInstance.VersionCriteria())
},
- manager.CurrentInstance.Name
+ manager.CurrentInstance.Name,
+ manager.CurrentInstance.game
);
Assert.That(mod_list, Has.Count.EqualTo(2));
@@ -187,7 +189,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash()
.Select(mod => new GUIMod(mod.Value.Latest(), registry, instance.KSP.VersionCriteria()))
.ToList();
- listGui.Rows.AddRange(modList.ConstructModList(modules, null).ToArray());
+ listGui.Rows.AddRange(modList.ConstructModList(modules, null, instance.KSP.game).ToArray());
// The header row adds one to the count
Assert.AreEqual(modules.Count + 1, listGui.Rows.Count);