Skip to content

Commit

Permalink
Game-specific labels
Browse files Browse the repository at this point in the history
Track labels' module identifiers per game
Create JsonToGamesDictionaryConverter
  • Loading branch information
HebaruSan committed Aug 20, 2023
1 parent f6d439f commit c18b1b0
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 24 deletions.
103 changes: 103 additions & 0 deletions Core/Converters/JsonToGamesDictionaryConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Linq;
using System.Collections;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace CKAN
{
/// <summary>
/// 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": {},
/// </summary>
public class JsonToGamesDictionaryConverter : JsonConverter
{
/// <summary>
/// Turn a tree of JSON tokens into a dictionary
/// </summary>
/// <param name="reader">Object that provides tokens to be translated</param>
/// <param name="objectType">The output type to be populated</param>
/// <param name="existingValue">Not used</param>
/// <param name="serializer">Generates output objects from tokens</param>
/// <returns>Dictionary of type matching the property where this converter was used, containing game-specific keys and values</returns>
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;
}

/// <summary>
/// We don't want to make any changes during serialization
/// </summary>
public override bool CanWrite => false;

/// <summary>
/// We don't want to make any changes during serialization
/// </summary>
/// <param name="writer">The object writing JSON to disk</param>
/// <param name="value">A value to be written for this class</param>
/// <param name="serializer">Generates output objects from tokens</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}

/// <summary>
/// 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.
/// </summary>
/// <returns>
/// false
/// </returns>
public override bool CanConvert(Type object_type) => false;

private static bool IsTokenEmpty(JToken token)
=> token.Type == JTokenType.Null
|| (token.Type == JTokenType.Array && !token.HasValues);
}
}
7 changes: 7 additions & 0 deletions Core/GameInstanceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -649,5 +649,12 @@ public IGame DetermineGame(DirectoryInfo path, IUser user)
public static IGame GameByShortName(string shortName)
=> knownGames.FirstOrDefault(g => g.ShortName == shortName);

/// <summary>
/// Return the short names of all known games
/// </summary>
/// <returns>Sequence of short name strings</returns>
public static IEnumerable<string> AllGameShortNames()
=> knownGames.Select(g => g.ShortName);

}
}
3 changes: 2 additions & 1 deletion GUI/Controls/Changeset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ private ListViewItem makeItem(ModChange change, Dictionary<CkanModule, string> 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,
Expand Down
8 changes: 4 additions & 4 deletions GUI/Controls/ManageMods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,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
)
{
Expand All @@ -271,7 +271,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,
}
Expand All @@ -289,11 +289,11 @@ 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)
{
Expand Down
2 changes: 1 addition & 1 deletion GUI/Controls/ModInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
54 changes: 49 additions & 5 deletions GUI/Labels/ModuleLabel.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -43,7 +50,30 @@ public class ModuleLabel
public bool HoldVersion;

[JsonProperty("module_identifiers_by_game", NullValueHandling = NullValueHandling.Ignore)]
public HashSet<string> ModuleIdentifiers = new HashSet<string>();
[JsonConverter(typeof(JsonToGamesDictionaryConverter))]
private Dictionary<string, HashSet<string>> ModuleIdentifiers =
new Dictionary<string, HashSet<string>>();

/// <summary>
/// Return the number of modules associated with this label for a given game
/// </summary>
/// <param name="game">Game to check</param>
/// <returns>Number of modules</returns>
public int ModuleCount(IGame game)
=> ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet<string> identifiers)
? identifiers.Count
: 0;

/// <summary>
/// Return whether a given identifier is associated with this label for a given game
/// </summary>
/// <param name="game">The game to check</param>
/// <param name="identifier">The identifier to check</param>
/// <returns>true if this label applies to this identifier, false otherwise</returns>
public bool ContainsModule(IGame game, string identifier)
=> ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet<string> identifiers)
? identifiers.Contains(identifier)
: false;

/// <summary>
/// Check whether this label is active for a given game instance
Expand All @@ -59,18 +89,32 @@ public bool AppliesTo(string instanceName)
/// Add a module to this label's group
/// </summary>
/// <param name="identifier">The identifier of the module to add</param>
public void Add(string identifier)
public void Add(IGame game, string identifier)
{
ModuleIdentifiers.Add(identifier);
if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet<string> identifiers))
{
identifiers.Add(identifier);
}
else
{
ModuleIdentifiers.Add(game.ShortName, new HashSet<string> {identifier});
}
}

/// <summary>
/// Remove a module from this label's group
/// </summary>
/// <param name="identifier">The identifier of the module to remove</param>
public void Remove(string identifier)
public void Remove(IGame game, string identifier)
{
ModuleIdentifiers.Remove(identifier);
if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet<string> identifiers))
{
identifiers.Remove(identifier);
if (identifiers.Count < 1)
{
ModuleIdentifiers.Remove(game.ShortName);
}
}
}
}

Expand Down
16 changes: 7 additions & 9 deletions GUI/Main/MainLabels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable<GUIMod> 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())
Expand All @@ -39,9 +39,9 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable<GUIMod> 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);
}
}
});
Expand All @@ -50,17 +50,15 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable<GUIMod> 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
}
Expand Down
6 changes: 3 additions & 3 deletions GUI/Model/ModList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ private bool HiddenByTagsOrLabels(GUIMod m, string instanceName)
// "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(Main.Instance.CurrentInstance.game, m.Identifier))
?? false)
|| (ModuleTags?.Tags?.Values
.Where(t => !TagInSearches(t) && t.Visible == false)
Expand Down Expand Up @@ -281,7 +281,7 @@ private DataGridViewRow MakeRow(GUIMod mod, List<ModChange> changes, string inst
DataGridViewRow item = new DataGridViewRow() {Tag = mod};

Color? myColor = ModuleLabels.LabelsFor(instanceName)
.FirstOrDefault(l => l.ModuleIdentifiers.Contains(mod.Identifier))
.FirstOrDefault(l => l.ContainsModule(Main.Instance.CurrentInstance.game, mod.Identifier))
?.Color;
if (myColor.HasValue)
{
Expand Down Expand Up @@ -380,7 +380,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;
Expand Down
3 changes: 2 additions & 1 deletion GUI/Model/ModSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit c18b1b0

Please sign in to comment.