Skip to content

Commit

Permalink
Merge #3885 Multi-game labels
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed Aug 21, 2023
2 parents 42875fb + 3bb37cf commit 858d71f
Show file tree
Hide file tree
Showing 24 changed files with 343 additions and 117 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions ConsoleUI/ExitScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions Core/CKANPathUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ namespace CKAN
{
public static class CKANPathUtils
{
/// <summary>
/// Path to save CKAN data shared across all game instances
/// </summary>
public static readonly string AppDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
Meta.GetProductName());

private static readonly ILog log = LogManager.GetLogger(typeof(CKANPathUtils));

/// <summary>
Expand Down
13 changes: 4 additions & 9 deletions Core/CompatibleGameVersions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,10 @@ class CompatibleGameVersions
public class CompatibleGameVersionsConverter : JsonPropertyNamesChangedConverter
{
protected override Dictionary<string, string> mapping
{
get
=> new Dictionary<string, string>
{
return new Dictionary<string, string>
{
{ "VersionOfKspWhenWritten", "GameVersionWhenWritten" },
{ "CompatibleKspVersions", "Versions" }
};
}
}
{ "VersionOfKspWhenWritten", "GameVersionWhenWritten" },
{ "CompatibleKspVersions", "Versions" }
};
}
}
15 changes: 4 additions & 11 deletions Core/Configuration/JsonConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 2 additions & 5 deletions Core/Configuration/Win32RegistryConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
78 changes: 68 additions & 10 deletions Core/Converters/JsonPropertyNamesChangedConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,53 @@
using System.Linq;
using System.Reflection;
using System.Collections.Generic;

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

namespace CKAN
{
/// <summary>
/// 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.
/// </summary>
public abstract class JsonPropertyNamesChangedConverter : JsonConverter
{
/// <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();
}

public override bool CanConvert(Type objectType)
{
return objectType.GetTypeInfo().IsClass;
}
/// <summary>
/// We only want to convert classes, not properties
/// </summary>
/// <param name="objectType">Type where this class been used as a JsonConverter</param>
/// <returns>true if it's a class, false otherwise</returns>
public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().IsClass;

/// <summary>
/// Parse JSON to an object, renaming properties according to the mapping property
/// </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>Class object populated according to the renaming scheme</returns>
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())
Expand All @@ -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<JsonPropertyAttribute>()?.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<JsonPropertyAttribute>()?.PropertyName ?? pi.Name) == name);
if (prop != null)
{
prop.SetValue(instance,
GetValue(prop.GetCustomAttribute<JsonConverterAttribute>(),
jp.Value, prop.PropertyType, serializer));
}
else
{
// No property, maybe there's a field
FieldInfo field = objectType.GetTypeInfo().DeclaredFields.FirstOrDefault(fi =>
(fi.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? fi.Name) == name);
if (field != null)
{
field.SetValue(instance,
GetValue(field.GetCustomAttribute<JsonConverterAttribute>(),
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);

/// <summary>
/// This is what you need to override in your child class
/// </summary>
/// <value>Mapping from old names to new names</value>
protected abstract Dictionary<string, string> mapping
{
get;
Expand Down
5 changes: 1 addition & 4 deletions Core/Converters/JsonSingleOrArrayConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
/// <returns>
/// false
/// </returns>
public override bool CanConvert(Type object_type)
{
return false;
}
public override bool CanConvert(Type object_type) => false;
}
}
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);
}
}
16 changes: 13 additions & 3 deletions Core/GameInstanceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

/// <summary>
/// Tries to determine the game that is installed at the given path
Expand Down Expand Up @@ -643,8 +641,20 @@ public IGame DetermineGame(DirectoryInfo path, IUser user)
}
}

/// <summary>
/// Return a game object based on its short name
/// </summary>
/// <param name="shortName">The short name to find</param>
/// <returns>A game object or null if none found</returns>
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);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
5 changes: 2 additions & 3 deletions Core/Games/KerbalSpaceProgram2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GameVersion> versions = JsonConvert.DeserializeObject<List<GameVersion>>(
File.Exists(cachedBuildMapPath)
Expand Down
Loading

0 comments on commit 858d71f

Please sign in to comment.