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 90babcedc1..67bf3a317a 100644
--- a/Core/GameInstanceManager.cs
+++ b/Core/GameInstanceManager.cs
@@ -649,5 +649,12 @@ public IGame DetermineGame(DirectoryInfo path, IUser user)
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/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 6858a69113..a49aa04e13 100644
--- a/GUI/Labels/ModuleLabel.cs
+++ b/GUI/Labels/ModuleLabel.cs
@@ -1,7 +1,14 @@
+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
{
@@ -43,7 +50,30 @@ public class ModuleLabel
public bool HoldVersion;
[JsonProperty("module_identifiers_by_game", NullValueHandling = NullValueHandling.Ignore)]
- public HashSet ModuleIdentifiers = new HashSet();
+ [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
@@ -59,18 +89,32 @@ public bool AppliesTo(string 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);
+ }
+ }
}
}
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);