Replace legacy custom JSON parser with System.Text.Json

- Rewrite Json.cs to use System.Text.Json.Nodes (JsonNode, JsonObject, JsonArray)
- Add JsonNodeExtensions.GetStringValue() for backward-compatible string access
- Update all 14 consumer files to use the new JsonNode API
- Remove ~300 lines of hand-rolled JSON parsing code from 2013
- Replace KeyUtils.EscapeString with delegation to Json.EscapeString

Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
Agent-Logs-Url: https://github.com/milutinke/Minecraft-Console-Client/sessions/afcd1b7b-ea23-4a0d-bb46-a90b623406fc
This commit is contained in:
copilot-swe-agent[bot] 2026-03-22 16:39:26 +00:00
parent 7893bd7fe4
commit 15aabd9423
15 changed files with 264 additions and 657 deletions

View file

@ -21,13 +21,13 @@ namespace MinecraftClient.Protocol
/// <returns></returns>
private static Dictionary<int, string> LoadRegistry(string registriesJsonFile, string jsonRegistryName)
{
Json.JSONData rawJson = Json.ParseJson(File.ReadAllText(registriesJsonFile));
Json.JSONData rawRegistry = rawJson.Properties[jsonRegistryName].Properties["entries"];
var rawJson = Json.ParseJson(File.ReadAllText(registriesJsonFile));
var rawRegistry = rawJson![jsonRegistryName]!["entries"]!.AsObject();
Dictionary<int, string> registry = new();
foreach (KeyValuePair<string, Json.JSONData> entry in rawRegistry.Properties)
foreach (var entry in rawRegistry)
{
int entryId = int.Parse(entry.Value.Properties["protocol_id"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
int entryId = int.Parse(entry.Value!["protocol_id"].GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture);
//minecraft:item_name => ItemName
string entryName = String.Concat(

View file

@ -63,7 +63,7 @@ namespace MinecraftClient.Protocol.Handlers.Forge
/// </summary>
/// <param name="data">The modinfo JSON tag.</param>
/// <param name="fmlVersion">Forge protocol version</param>
internal ForgeInfo(Json.JSONData data, FMLVersion fmlVersion)
internal ForgeInfo(System.Text.Json.Nodes.JsonObject data, FMLVersion fmlVersion)
{
Mods = new List<ForgeMod>();
Version = fmlVersion;
@ -91,10 +91,10 @@ namespace MinecraftClient.Protocol.Handlers.Forge
// }]
// }
foreach (Json.JSONData mod in data.Properties["modList"].DataArray)
foreach (var mod in data["modList"]!.AsArray())
{
String modid = mod.Properties["modid"].StringValue;
String modversion = mod.Properties["version"].StringValue;
String modid = mod!["modid"]!.GetStringValue();
String modversion = mod["version"]!.GetStringValue();
Mods.Add(new ForgeMod(modid, modversion));
}
@ -131,10 +131,10 @@ namespace MinecraftClient.Protocol.Handlers.Forge
// "fmlNetworkVersion": 2
// }
foreach (Json.JSONData mod in data.Properties["mods"].DataArray)
foreach (var mod in data["mods"]!.AsArray())
{
String modid = mod.Properties["modId"].StringValue;
String modmarker = mod.Properties["modmarker"].StringValue;
String modid = mod!["modId"]!.GetStringValue();
String modmarker = mod["modmarker"]!.GetStringValue();
Mods.Add(new ForgeMod(modid, modmarker));
}
@ -157,7 +157,7 @@ namespace MinecraftClient.Protocol.Handlers.Forge
// - Here is the discussion:
// see https://github.com/MinecraftForge/MinecraftForge/pull/8169
string encodedData = data.Properties["d"].StringValue;
string encodedData = data["d"]!.GetStringValue();
Queue<byte> dataPackage = decodeOptimized(encodedData);
DataTypes dataTypes = new DataTypes(Protocol18Handler.MC_1_18_1_Version);

View file

@ -1031,10 +1031,10 @@ namespace MinecraftClient.Protocol.Handlers
? dataTypes.ReadNextString(packetData)
: null;
var chatInfo = Json.ParseJson(chatName).Properties;
var senderDisplayName = chatInfo != null && chatInfo.Count > 0
var chatInfo = Json.ParseJson(chatName)?.AsObject();
var senderDisplayName = chatInfo is not null && chatInfo.Count > 0
? (chatInfo.ContainsKey("insertion") ? chatInfo["insertion"] : chatInfo["text"])
.StringValue
.GetStringValue()
: "";
string? senderTeamName = null;
var messageTypeEnum =
@ -1043,8 +1043,8 @@ namespace MinecraftClient.Protocol.Handlers
if (targetName != null &&
(messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING ||
messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING))
senderTeamName = Json.ParseJson(targetName).Properties["with"].DataArray[0]
.Properties["text"].StringValue;
senderTeamName = Json.ParseJson(targetName)!["with"]![0]!
["text"]!.GetStringValue();
if (string.IsNullOrWhiteSpace(senderDisplayName))
{
@ -3574,22 +3574,22 @@ namespace MinecraftClient.Protocol.Handlers
if (string.IsNullOrEmpty(result) || !result.StartsWith("{") || !result.EndsWith("}")) return false;
var jsonData = Json.ParseJson(result);
if (jsonData.Type != Json.JSONData.DataType.Object || !jsonData.Properties.ContainsKey("version"))
if (jsonData is not System.Text.Json.Nodes.JsonObject jsonObj || !jsonObj.ContainsKey("version"))
return false;
var versionData = jsonData.Properties["version"];
var versionData = jsonObj["version"]!.AsObject();
//Retrieve display name of the Minecraft version
if (versionData.Properties.TryGetValue("name", out var property))
version = property.StringValue;
if (versionData["name"] is { } nameNode)
version = nameNode.GetStringValue();
//Retrieve protocol version number for handling this server
if (versionData.Properties.TryGetValue("protocol", out var dataProperty))
protocolVersion = int.Parse(dataProperty.StringValue,
if (versionData["protocol"] is { } protocolNode)
protocolVersion = int.Parse(protocolNode.GetStringValue(),
NumberStyles.Any, CultureInfo.CurrentCulture);
// Check for forge on the server.
Protocol18Forge.ServerInfoCheckForge(jsonData, ref forgeInfo);
Protocol18Forge.ServerInfoCheckForge(jsonObj, ref forgeInfo);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_server_protocol, version,
protocolVersion + (forgeInfo != null ? Translations.mcc_with_forge : "")));

View file

@ -484,7 +484,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <returns>True if the server is running Forge</returns>
public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo? forgeInfo)
public static bool ServerInfoCheckForge(System.Text.Json.Nodes.JsonObject jsonData, ref ForgeInfo? forgeInfo)
{
return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower
|| ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2) // MC 1.13 to 1.17
@ -530,7 +530,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <param name="fmlVersion">Forge protocol version</param>
/// <returns>True if the server is running Forge</returns>
private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo? forgeInfo, FMLVersion fmlVersion)
private static bool ServerInfoCheckForgeSub(System.Text.Json.Nodes.JsonObject jsonData, ref ForgeInfo? forgeInfo, FMLVersion fmlVersion)
{
string forgeDataTag;
string versionField;
@ -557,10 +557,9 @@ namespace MinecraftClient.Protocol.Handlers
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
}
if (jsonData.Properties.ContainsKey(forgeDataTag) && jsonData.Properties[forgeDataTag].Type == Json.JSONData.DataType.Object)
if (jsonData[forgeDataTag] is System.Text.Json.Nodes.JsonObject modData)
{
Json.JSONData modData = jsonData.Properties[forgeDataTag];
if (modData.Properties.ContainsKey(versionField) && modData.Properties[versionField].StringValue == versionString)
if (modData[versionField] is not null && modData[versionField]!.GetStringValue() == versionString)
{
forgeInfo = new ForgeInfo(modData, fmlVersion);
if (forgeInfo.Mods.Any())

View file

@ -436,74 +436,70 @@ namespace MinecraftClient.Protocol.Message
/// <param name="colorcode">Allow parent color code to affect child elements (set to "" for function init)</param>
/// <param name="links">Container for links from JSON serialized text</param>
/// <returns>returns the Minecraft-formatted string</returns>
private static string JSONData2String(Json.JSONData data, string colorcode, List<string>? links)
private static string JSONData2String(System.Text.Json.Nodes.JsonNode? data, string colorcode, List<string>? links)
{
string extra_result = "";
switch (data.Type)
switch (data)
{
case Json.JSONData.DataType.Object:
if (data.Properties.ContainsKey("color"))
case System.Text.Json.Nodes.JsonObject obj:
if (obj.ContainsKey("color"))
{
colorcode = Color2tag(JSONData2String(data.Properties["color"], "", links));
colorcode = Color2tag(JSONData2String(obj["color"], "", links));
}
if (data.Properties.ContainsKey("clickEvent") && links != null)
if (obj.ContainsKey("clickEvent") && links is not null)
{
Json.JSONData clickEvent = data.Properties["clickEvent"];
if (clickEvent.Properties.ContainsKey("action")
&& clickEvent.Properties.ContainsKey("value")
&& clickEvent.Properties["action"].StringValue == "open_url"
&& !string.IsNullOrEmpty(clickEvent.Properties["value"].StringValue))
var clickEvent = obj["clickEvent"]!.AsObject();
if (clickEvent.ContainsKey("action")
&& clickEvent.ContainsKey("value")
&& clickEvent["action"]!.GetStringValue() == "open_url"
&& !string.IsNullOrEmpty(clickEvent["value"]!.GetStringValue()))
{
links.Add(clickEvent.Properties["value"].StringValue);
links.Add(clickEvent["value"]!.GetStringValue());
}
}
if (data.Properties.ContainsKey("extra"))
if (obj.ContainsKey("extra"))
{
Json.JSONData[] extras = data.Properties["extra"].DataArray.ToArray();
foreach (Json.JSONData item in extras)
foreach (var item in obj["extra"]!.AsArray())
extra_result = extra_result + JSONData2String(item, colorcode, links) + "§r";
}
if (data.Properties.ContainsKey("text"))
if (obj.ContainsKey("text"))
{
return colorcode + JSONData2String(data.Properties["text"], colorcode, links) + extra_result;
return colorcode + JSONData2String(obj["text"], colorcode, links) + extra_result;
}
else if (data.Properties.ContainsKey("translate"))
else if (obj.ContainsKey("translate"))
{
List<string> using_data = new();
if (data.Properties.ContainsKey("using") && !data.Properties.ContainsKey("with"))
data.Properties["with"] = data.Properties["using"];
if (data.Properties.ContainsKey("with"))
if (obj.ContainsKey("using") && !obj.ContainsKey("with"))
obj["with"] = System.Text.Json.Nodes.JsonNode.Parse(obj["using"]!.ToJsonString());
if (obj.ContainsKey("with"))
{
Json.JSONData[] array = data.Properties["with"].DataArray.ToArray();
for (int i = 0; i < array.Length; i++)
foreach (var item in obj["with"]!.AsArray())
{
using_data.Add(JSONData2String(array[i], colorcode, links));
using_data.Add(JSONData2String(item, colorcode, links));
}
}
return colorcode +
TranslateString(JSONData2String(data.Properties["translate"], "", links), using_data) +
TranslateString(JSONData2String(obj["translate"], "", links), using_data) +
extra_result;
}
else return extra_result;
case Json.JSONData.DataType.Array:
case System.Text.Json.Nodes.JsonArray arr:
string result = "";
foreach (Json.JSONData item in data.DataArray)
foreach (var item in arr)
{
result += JSONData2String(item, colorcode, links);
}
return result;
case Json.JSONData.DataType.String:
return colorcode + data.StringValue;
default:
return colorcode + data.GetStringValue();
}
return "";
}
private static string NbtToString(Dictionary<string, object> nbt)

View file

@ -68,20 +68,20 @@ namespace MinecraftClient.Protocol
var jsonData = Json.ParseJson(response.Body);
// Error handling
if (jsonData.Properties.ContainsKey("error"))
if (jsonData?["error"] is not null)
{
throw new Exception(jsonData.Properties["error_description"].StringValue);
throw new Exception(jsonData["error_description"].GetStringValue());
}
else
{
string accessToken = jsonData.Properties["access_token"].StringValue;
string refreshToken = jsonData.Properties["refresh_token"].StringValue;
int expiresIn = int.Parse(jsonData.Properties["expires_in"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
string accessToken = jsonData!["access_token"]!.GetStringValue();
string refreshToken = jsonData["refresh_token"]!.GetStringValue();
int expiresIn = int.Parse(jsonData["expires_in"].GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture);
// Extract email from JWT
string payload = JwtPayloadDecode.GetPayload(jsonData.Properties["id_token"].StringValue);
string payload = JwtPayloadDecode.GetPayload(jsonData["id_token"]!.GetStringValue());
var jsonPayload = Json.ParseJson(payload);
string email = jsonPayload.Properties["email"].StringValue;
string email = jsonPayload!["email"]!.GetStringValue();
return new LoginResponse()
{
Email = email,
@ -299,9 +299,9 @@ namespace MinecraftClient.Protocol
string jsonString = response.Body;
//Console.WriteLine(jsonString);
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
var json = Json.ParseJson(jsonString);
string token = json!["Token"]!.GetStringValue();
string userHash = json["DisplayClaims"]!["xui"]![0]!["uhs"]!.GetStringValue();
return new XblAuthenticateResponse()
{
Token = token,
@ -347,9 +347,9 @@ namespace MinecraftClient.Protocol
if (response.StatusCode == 200)
{
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
var json = Json.ParseJson(jsonString);
string token = json!["Token"]!.GetStringValue();
string userHash = json["DisplayClaims"]!["xui"]![0]!["uhs"]!.GetStringValue();
return new XSTSAuthenticateResponse()
{
Token = token,
@ -360,16 +360,16 @@ namespace MinecraftClient.Protocol
{
if (response.StatusCode == 401)
{
Json.JSONData json = Json.ParseJson(response.Body);
if (json.Properties["XErr"].StringValue == "2148916233")
var json = Json.ParseJson(response.Body);
if (json!["XErr"]!.GetStringValue() == "2148916233")
{
throw new Exception("The account doesn't have an Xbox account");
}
else if (json.Properties["XErr"].StringValue == "2148916238")
else if (json["XErr"]!.GetStringValue() == "2148916238")
{
throw new Exception("The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult");
}
else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue);
else throw new Exception("Unknown XSTS error code: " + json["XErr"]!.GetStringValue());
}
else
{
@ -426,9 +426,9 @@ namespace MinecraftClient.Protocol
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
var json = Json.ParseJson(jsonString);
return json.Properties["access_token"].StringValue;
return json!["access_token"]!.GetStringValue();
}
/// <summary>
@ -448,8 +448,8 @@ namespace MinecraftClient.Protocol
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
return json.Properties["items"].DataArray.Count > 0;
var json = Json.ParseJson(jsonString);
return json!["items"]!.AsArray().Count > 0;
}
public static UserProfile GetUserProfile(string accessToken)
@ -464,11 +464,11 @@ namespace MinecraftClient.Protocol
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
var json = Json.ParseJson(jsonString);
return new UserProfile()
{
UUID = json.Properties["id"].StringValue,
UserName = json.Properties["name"].StringValue
UUID = json!["id"]!.GetStringValue(),
UserName = json["name"]!.GetStringValue()
};
}

View file

@ -121,7 +121,7 @@ namespace MinecraftClient.Protocol
{
Task<string> fetchTask = httpClient.GetStringAsync("https://api.mojang.com/users/profiles/minecraft/" + name);
fetchTask.Wait();
string result = Json.ParseJson(fetchTask.Result).Properties["id"].StringValue;
string result = Json.ParseJson(fetchTask.Result)!["id"]!.GetStringValue();
fetchTask.Dispose();
return result;
}
@ -140,11 +140,11 @@ namespace MinecraftClient.Protocol
{
Task<string> fetchTask = httpClient.GetStringAsync("https://api.mojang.com/user/profiles/" + uuid + "/names");
fetchTask.Wait();
var nameChanges = Json.ParseJson(fetchTask.Result).DataArray;
var nameChanges = Json.ParseJson(fetchTask.Result)!.AsArray();
fetchTask.Dispose();
// Names are sorted from past to most recent. We need to get the last name in the list
return nameChanges[^1].Properties["name"].StringValue;
return nameChanges[^1]!["name"]!.GetStringValue();
}
catch (Exception) { return string.Empty; }
}
@ -157,40 +157,32 @@ namespace MinecraftClient.Protocol
public static Dictionary<string, DateTime> UuidToNameHistory(string uuid)
{
Dictionary<string, DateTime> tempDict = new();
List<Json.JSONData> jsonDataList;
System.Text.Json.Nodes.JsonArray jsonDataList;
// Perform web request
try
{
Task<string> fetchTask = httpClient.GetStringAsync("https://api.mojang.com/user/profiles/" + uuid + "/names");
fetchTask.Wait();
jsonDataList = Json.ParseJson(fetchTask.Result).DataArray;
jsonDataList = Json.ParseJson(fetchTask.Result)!.AsArray();
fetchTask.Dispose();
}
catch (Exception) { return tempDict; }
foreach (Json.JSONData jsonData in jsonDataList)
foreach (var jsonData in jsonDataList)
{
if (jsonData.Properties.Count > 1)
var obj = jsonData!.AsObject();
if (obj.Count > 1)
{
// Time is saved as long in the Unix format.
// Convert it to normal time, before adding it to the dictionary.
//
// !! FromUnixTimeMilliseconds does not exist in the current version. !!
// DateTimeOffset creationDate = DateTimeOffset.FromUnixTimeMilliseconds(Convert.ToInt64(jsonData.Properties["changedToAt"].StringValue));
//
DateTimeOffset creationDate = UnixTimeStampToDateTime(Convert.ToDouble(jsonData["changedToAt"].GetStringValue()));
// Workaround for converting Unix time to normal time.
DateTimeOffset creationDate = UnixTimeStampToDateTime(Convert.ToDouble(jsonData.Properties["changedToAt"].StringValue));
// Add Keyvaluepair to dict.
tempDict.Add(jsonData.Properties["name"].StringValue, creationDate.DateTime);
tempDict.Add(jsonData["name"]!.GetStringValue(), creationDate.DateTime);
}
// The first entry does not contain a change date.
else if (jsonData.Properties.Count > 0)
else if (obj.Count > 0)
{
// Add an undefined time to it.
tempDict.Add(jsonData.Properties["name"].StringValue, new DateTime());
tempDict.Add(jsonData["name"]!.GetStringValue(), new DateTime());
}
}
@ -203,14 +195,14 @@ namespace MinecraftClient.Protocol
/// <returns>Dictionary of the Mojang services</returns>
public static MojangServiceStatus GetMojangServiceStatus()
{
List<Json.JSONData> jsonDataList;
System.Text.Json.Nodes.JsonArray jsonDataList;
// Perform web request
try
{
Task<string> fetchTask = httpClient.GetStringAsync("https://status.mojang.com/check");
fetchTask.Wait();
jsonDataList = Json.ParseJson(fetchTask.Result).DataArray;
jsonDataList = Json.ParseJson(fetchTask.Result)!.AsArray();
fetchTask.Dispose();
}
catch (Exception)
@ -219,14 +211,14 @@ namespace MinecraftClient.Protocol
}
// Convert string to enum values and store them inside a MojangeServiceStatus object.
return new MojangServiceStatus(minecraftNet: StringToServiceStatus(jsonDataList[0].Properties["minecraft.net"].StringValue),
sessionMinecraftNet: StringToServiceStatus(jsonDataList[1].Properties["session.minecraft.net"].StringValue),
accountMojangCom: StringToServiceStatus(jsonDataList[2].Properties["account.mojang.com"].StringValue),
authserverMojangCom: StringToServiceStatus(jsonDataList[3].Properties["authserver.mojang.com"].StringValue),
sessionserverMojangCom: StringToServiceStatus(jsonDataList[4].Properties["sessionserver.mojang.com"].StringValue),
apiMojangCom: StringToServiceStatus(jsonDataList[5].Properties["api.mojang.com"].StringValue),
texturesMinecraftNet: StringToServiceStatus(jsonDataList[6].Properties["textures.minecraft.net"].StringValue),
mojangCom: StringToServiceStatus(jsonDataList[7].Properties["mojang.com"].StringValue)
return new MojangServiceStatus(minecraftNet: StringToServiceStatus(jsonDataList[0]!["minecraft.net"]!.GetStringValue()),
sessionMinecraftNet: StringToServiceStatus(jsonDataList[1]!["session.minecraft.net"]!.GetStringValue()),
accountMojangCom: StringToServiceStatus(jsonDataList[2]!["account.mojang.com"]!.GetStringValue()),
authserverMojangCom: StringToServiceStatus(jsonDataList[3]!["authserver.mojang.com"]!.GetStringValue()),
sessionserverMojangCom: StringToServiceStatus(jsonDataList[4]!["sessionserver.mojang.com"]!.GetStringValue()),
apiMojangCom: StringToServiceStatus(jsonDataList[5]!["api.mojang.com"]!.GetStringValue()),
texturesMinecraftNet: StringToServiceStatus(jsonDataList[6]!["textures.minecraft.net"]!.GetStringValue()),
mojangCom: StringToServiceStatus(jsonDataList[7]!["mojang.com"]!.GetStringValue())
);
}
@ -237,9 +229,9 @@ namespace MinecraftClient.Protocol
/// <returns>Dictionary with a link to the skin and cape of a player.</returns>
public static SkinInfo GetSkinInfo(string uuid)
{
Dictionary<string, Json.JSONData> textureDict;
System.Text.Json.Nodes.JsonObject textureObj;
string base64SkinInfo;
Json.JSONData decodedJsonSkinInfo;
System.Text.Json.Nodes.JsonNode? decodedJsonSkinInfo;
// Perform web request
try
@ -247,7 +239,7 @@ namespace MinecraftClient.Protocol
Task<string> fetchTask = httpClient.GetStringAsync("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
fetchTask.Wait();
// Obtain the Base64 encoded skin information from the API. Discard the rest, since it can be obtained easier through other requests.
base64SkinInfo = Json.ParseJson(fetchTask.Result).Properties["properties"].DataArray[0].Properties["value"].StringValue;
base64SkinInfo = Json.ParseJson(fetchTask.Result)!["properties"]![0]!["value"]!.GetStringValue();
fetchTask.Dispose();
}
catch (Exception) { return new SkinInfo(); }
@ -257,23 +249,18 @@ namespace MinecraftClient.Protocol
// Assert temporary variable for readablity.
// Contains skin and cape information.
textureDict = decodedJsonSkinInfo.Properties["textures"].Properties;
textureObj = decodedJsonSkinInfo!["textures"]!.AsObject();
// Can apparently be missing, if no custom skin is set.
// Probably for completely new accounts.
// (Still exists after changing back to Steve or Alex skin.)
if (textureDict.ContainsKey("SKIN"))
if (textureObj.ContainsKey("SKIN"))
{
return new SkinInfo(skinUrl: textureDict["SKIN"].Properties.ContainsKey("url") ? textureDict["SKIN"].Properties["url"].StringValue : string.Empty,
capeUrl: textureDict.ContainsKey("CAPE") ? textureDict["CAPE"].Properties["url"].StringValue : string.Empty,
skinModel: textureDict["SKIN"].Properties.ContainsKey("metadata") ? "Alex" : "Steve");
return new SkinInfo(skinUrl: textureObj["SKIN"]!["url"] is not null ? textureObj["SKIN"]!["url"]!.GetStringValue() : string.Empty,
capeUrl: textureObj.ContainsKey("CAPE") ? textureObj["CAPE"]!["url"]!.GetStringValue() : string.Empty,
skinModel: textureObj["SKIN"]!["metadata"] is not null ? "Alex" : "Steve");
}
// Tested it on several players, this case never occured.
else
{
// This player has assumingly never changed their skin.
// Probably a completely new account.
return new SkinInfo(capeUrl: textureDict.ContainsKey("CAPE") ? textureDict["CAPE"].Properties["url"].StringValue : string.Empty,
return new SkinInfo(capeUrl: textureObj.ContainsKey("CAPE") ? textureObj["CAPE"]!["url"]!.GetStringValue() : string.Empty,
skinModel: DefaultModelAlex(uuid) ? "Alex" : "Steve");
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Message;
using static MinecraftClient.Protocol.Message.LastSeenMessageList;
@ -37,17 +38,17 @@ namespace MinecraftClient.Protocol.ProfileKey
// see https://github.com/yushijinhun/authlib-injector/blob/da910956eaa30d2f6c2c457222d188aeb53b0d1f/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java#L49
// POST to "https://api.minecraftservices.com/player/certificates" with authlib-injector will get a dummy response
Json.JSONData json = isYggdrasil ? MakeDummyResponse() : Json.ParseJson(response!.Body);
var json = isYggdrasil ? MakeDummyResponse() : Json.ParseJson(response!.Body);
// Error here
PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue,
sig: json.Properties["publicKeySignature"].StringValue,
sigV2: json.Properties["publicKeySignatureV2"].StringValue);
PublicKey publicKey = new(pemKey: json!["keyPair"]!["publicKey"]!.GetStringValue(),
sig: json["publicKeySignature"]!.GetStringValue(),
sigV2: json["publicKeySignatureV2"]!.GetStringValue());
PrivateKey privateKey = new(pemKey: json.Properties["keyPair"].Properties["privateKey"].StringValue);
PrivateKey privateKey = new(pemKey: json["keyPair"]!["privateKey"]!.GetStringValue());
return new PlayerKeyPair(publicKey, privateKey,
expiresAt: json.Properties["expiresAt"].StringValue,
refreshedAfter: json.Properties["refreshedAfter"].StringValue);
expiresAt: json["expiresAt"]!.GetStringValue(),
refreshedAfter: json["refreshedAfter"]!.GetStringValue());
}
catch (Exception e)
{
@ -191,51 +192,10 @@ namespace MinecraftClient.Protocol.ProfileKey
return data.ToArray();
}
// https://github.com/mono/mono/blob/master/mcs/class/System.Json/System.Json/JsonValue.cs
public static string EscapeString(string src)
{
StringBuilder sb = new();
// Delegate to the shared Json.EscapeString backed by System.Text.Json
public static string EscapeString(string src) => Json.EscapeString(src);
int start = 0;
for (int i = 0; i < src.Length; i++)
{
char c = src[i];
bool needEscape = c < 32 || c == '"' || c == '\\';
// Broken lead surrogate
needEscape = needEscape || c >= '\uD800' && c <= '\uDBFF' &&
(i == src.Length - 1 || src[i + 1] < '\uDC00' || src[i + 1] > '\uDFFF');
// Broken tail surrogate
needEscape = needEscape || c >= '\uDC00' && c <= '\uDFFF' &&
(i == 0 || src[i - 1] < '\uD800' || src[i - 1] > '\uDBFF');
// To produce valid JavaScript
needEscape = needEscape || c == '\u2028' || c == '\u2029';
if (needEscape)
{
sb.Append(src, start, i - start);
switch (src[i])
{
case '\b': sb.Append("\\b"); break;
case '\f': sb.Append("\\f"); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
case '\"': sb.Append("\\\""); break;
case '\\': sb.Append("\\\\"); break;
default:
sb.Append("\\u");
sb.Append(((int)src[i]).ToString("x04"));
break;
}
start = i + 1;
}
}
sb.Append(src, start, src.Length - start);
return sb.ToString();
}
public static Json.JSONData MakeDummyResponse()
public static JsonNode MakeDummyResponse()
{
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048);
var mimePublicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo());
@ -245,19 +205,20 @@ namespace MinecraftClient.Protocol.ProfileKey
DateTime now = DateTime.UtcNow;
DateTime expiresAt = now.AddHours(48);
DateTime refreshedAfter = now.AddHours(36);
Json.JSONData response = new(Json.JSONData.DataType.Object);
Json.JSONData keyPairObj = new(Json.JSONData.DataType.Object);
keyPairObj.Properties["privateKey"] = new(Json.JSONData.DataType.String){ StringValue = privateKeyPEM };
keyPairObj.Properties["publicKey"] = new(Json.JSONData.DataType.String){ StringValue = publicKeyPEM };
response.Properties["keyPair"] = keyPairObj;
response.Properties["publicKeySignature"] = new(Json.JSONData.DataType.String){ StringValue = "AA==" };
response.Properties["publicKeySignatureV2"] = new(Json.JSONData.DataType.String){ StringValue = "AA==" };
string format = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
response.Properties["expiresAt"] = new(Json.JSONData.DataType.String){ StringValue = expiresAt.ToString(format) };
response.Properties["refreshedAfter"] = new(Json.JSONData.DataType.String){ StringValue = refreshedAfter.ToString(format) };
return response;
return new JsonObject
{
["keyPair"] = new JsonObject
{
["privateKey"] = privateKeyPEM,
["publicKey"] = publicKeyPEM
},
["publicKeySignature"] = "AA==",
["publicKeySignatureV2"] = "AA==",
["expiresAt"] = expiresAt.ToString(format),
["refreshedAfter"] = refreshedAfter.ToString(format)
};
}
}
}

View file

@ -581,16 +581,15 @@ namespace MinecraftClient.Protocol
}
else
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken")
&& loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
var loginResponse = Json.ParseJson(result);
if (loginResponse?["accessToken"] is not null
&& loginResponse["selectedProfile"]?["id"] is not null
&& loginResponse["selectedProfile"]?["name"] is not null)
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"]
.StringValue;
session.ID = loginResponse["accessToken"]!.GetStringValue();
session.PlayerID = loginResponse["selectedProfile"]!["id"]!.GetStringValue();
session.PlayerName = loginResponse["selectedProfile"]!["name"]!
.GetStringValue();
return LoginResult.Success;
}
else return LoginResult.InvalidResponse;
@ -667,27 +666,25 @@ namespace MinecraftClient.Protocol
}
else
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken"))
var loginResponse = Json.ParseJson(result);
if (loginResponse?["accessToken"] is not null)
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
if (loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
session.ID = loginResponse["accessToken"]!.GetStringValue();
if (loginResponse["selectedProfile"]?["id"] is not null
&& loginResponse["selectedProfile"]?["name"] is not null)
{
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"]
.StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"]
.StringValue;
session.PlayerID = loginResponse["selectedProfile"]!["id"]!
.GetStringValue();
session.PlayerName = loginResponse["selectedProfile"]!["name"]!
.GetStringValue();
return LoginResult.Success;
}
else
{
string availableProfiles = "";
foreach (Json.JSONData profile in loginResponse.Properties["availableProfiles"]
.DataArray)
foreach (var profile in loginResponse["availableProfiles"]!.AsArray())
{
availableProfiles += " " + profile.Properties["name"].StringValue;
availableProfiles += " " + profile!["name"]!.GetStringValue();
}
ConsoleIO.WriteLine(Translations.mcc_avaliable_profiles + availableProfiles);
@ -703,19 +700,18 @@ namespace MinecraftClient.Protocol
ConsoleIO.WriteLine(Translations.mcc_selected_profile + " " + selectedProfileName);
Json.JSONData? selectedProfile = null;
foreach (Json.JSONData profile in loginResponse.Properties["availableProfiles"]
.DataArray)
System.Text.Json.Nodes.JsonNode? selectedProfile = null;
foreach (var profile in loginResponse["availableProfiles"]!.AsArray())
{
selectedProfile = profile.Properties["name"].StringValue == selectedProfileName
selectedProfile = profile!["name"]!.GetStringValue() == selectedProfileName
? profile
: selectedProfile;
}
if (selectedProfile != null)
if (selectedProfile is not null)
{
session.PlayerID = selectedProfile.Properties["id"].StringValue;
session.PlayerName = selectedProfile.Properties["name"].StringValue;
session.PlayerID = selectedProfile["id"]!.GetStringValue();
session.PlayerName = selectedProfile["name"]!.GetStringValue();
SessionToken currentsession = session;
return GetNewYggdrasilToken(currentsession, out session);
}
@ -889,7 +885,7 @@ namespace MinecraftClient.Protocol
{
var payload = JwtPayloadDecode.GetPayload(session.ID);
var json = Json.ParseJson(payload);
var expTimestamp = long.Parse(json.Properties["exp"].StringValue, NumberStyles.Any,
var expTimestamp = long.Parse(json!["exp"]!.GetStringValue(), NumberStyles.Any,
CultureInfo.CurrentCulture);
var now = DateTime.Now;
var tokenExp = UnixTimeStampToDateTime(expTimestamp);
@ -935,16 +931,15 @@ namespace MinecraftClient.Protocol
}
else
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken")
&& loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
var loginResponse = Json.ParseJson(result);
if (loginResponse?["accessToken"] is not null
&& loginResponse["selectedProfile"]?["id"] is not null
&& loginResponse["selectedProfile"]?["name"] is not null)
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"]
.StringValue;
session.ID = loginResponse["accessToken"]!.GetStringValue();
session.PlayerID = loginResponse["selectedProfile"]!["id"]!.GetStringValue();
session.PlayerName = loginResponse["selectedProfile"]!["name"]!
.GetStringValue();
return LoginResult.Success;
}
else return LoginResult.InvalidResponse;
@ -986,16 +981,15 @@ namespace MinecraftClient.Protocol
}
else
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken")
&& loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
var loginResponse = Json.ParseJson(result);
if (loginResponse?["accessToken"] is not null
&& loginResponse["selectedProfile"]?["id"] is not null
&& loginResponse["selectedProfile"]?["name"] is not null)
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"]
.StringValue;
session.ID = loginResponse["accessToken"]!.GetStringValue();
session.PlayerID = loginResponse["selectedProfile"]!["id"]!.GetStringValue();
session.PlayerName = loginResponse["selectedProfile"]!["name"]!
.GetStringValue();
return LoginResult.Success;
}
else return LoginResult.InvalidResponse;
@ -1065,28 +1059,27 @@ namespace MinecraftClient.Protocol
string cookies = String.Format("sid=token:{0}:{1};user={2};version={3}", accesstoken, uuid, username,
Program.MCHighestVersion);
DoHTTPSGet("pc.realms.minecraft.net", 443, "/worlds", cookies, ref result);
Json.JSONData realmsWorlds = Json.ParseJson(result);
if (realmsWorlds.Properties.ContainsKey("servers")
&& realmsWorlds.Properties["servers"].Type == Json.JSONData.DataType.Array
&& realmsWorlds.Properties["servers"].DataArray.Count > 0)
var realmsWorlds = Json.ParseJson(result);
if (realmsWorlds?["servers"] is System.Text.Json.Nodes.JsonArray serversArray
&& serversArray.Count > 0)
{
List<string> availableWorlds = new(); // Store string to print
int index = 0;
foreach (Json.JSONData realmsServer in realmsWorlds.Properties["servers"].DataArray)
foreach (var realmsServer in serversArray)
{
if (realmsServer.Properties.ContainsKey("name")
&& realmsServer.Properties.ContainsKey("owner")
&& realmsServer.Properties.ContainsKey("id")
&& realmsServer.Properties.ContainsKey("expired"))
if (realmsServer?["name"] is not null
&& realmsServer["owner"] is not null
&& realmsServer["id"] is not null
&& realmsServer["expired"] is not null)
{
if (realmsServer.Properties["expired"].StringValue == "false")
if (realmsServer["expired"].GetStringValue() == "false")
{
availableWorlds.Add(String.Format("[{0}] {2} ({3}) - {1}",
index++,
realmsServer.Properties["id"].StringValue,
realmsServer.Properties["name"].StringValue,
realmsServer.Properties["owner"].StringValue));
realmsWorldsResult.Add(realmsServer.Properties["id"].StringValue);
realmsServer["id"]!.GetStringValue(),
realmsServer["name"]!.GetStringValue(),
realmsServer["owner"]!.GetStringValue()));
realmsWorldsResult.Add(realmsServer["id"]!.GetStringValue());
}
}
}
@ -1132,9 +1125,9 @@ namespace MinecraftClient.Protocol
cookies, ref result);
if (statusCode == 200)
{
Json.JSONData serverAddress = Json.ParseJson(result);
if (serverAddress.Properties.ContainsKey("address"))
return serverAddress.Properties["address"].StringValue;
var serverAddress = Json.ParseJson(result);
if (serverAddress?["address"] is not null)
return serverAddress["address"]!.GetStringValue();
else
{
ConsoleIO.WriteLine(Translations.error_realms_ip_error);

View file

@ -123,35 +123,36 @@ namespace MinecraftClient.Protocol.Session
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft)));
Json.JSONData mcSession = new(Json.JSONData.DataType.String);
System.Text.Json.Nodes.JsonNode? mcSession = null;
try
{
mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft));
}
catch (IOException) { /* Failed to read file from disk -- ignoring */ }
if (mcSession.Type == Json.JSONData.DataType.Object
&& mcSession.Properties.ContainsKey("clientToken")
&& mcSession.Properties.ContainsKey("authenticationDatabase"))
if (mcSession is System.Text.Json.Nodes.JsonObject mcSessionObj
&& mcSessionObj.ContainsKey("clientToken")
&& mcSessionObj.ContainsKey("authenticationDatabase"))
{
string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", "");
Dictionary<string, Json.JSONData> sessionItems = mcSession.Properties["authenticationDatabase"].Properties;
foreach (string key in sessionItems.Keys)
string clientID = mcSession["clientToken"]!.GetStringValue().Replace("-", "");
var sessionItems = mcSession["authenticationDatabase"]!.AsObject();
foreach (var kvp in sessionItems)
{
string key = kvp.Key;
if (Guid.TryParseExact(key, "N", out Guid temp))
{
Dictionary<string, Json.JSONData> sessionItem = sessionItems[key].Properties;
var sessionItem = kvp.Value!.AsObject();
if (sessionItem.ContainsKey("displayName")
&& sessionItem.ContainsKey("accessToken")
&& sessionItem.ContainsKey("username")
&& sessionItem.ContainsKey("uuid"))
{
string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue);
string login = Settings.ToLowerIfNeed(sessionItem["username"]!.GetStringValue());
try
{
SessionToken session = SessionToken.FromString(String.Join(",",
sessionItem["accessToken"].StringValue,
sessionItem["displayName"].StringValue,
sessionItem["uuid"].StringValue.Replace("-", ""),
sessionItem["accessToken"]!.GetStringValue(),
sessionItem["displayName"]!.GetStringValue(),
sessionItem["uuid"]!.GetStringValue().Replace("-", ""),
clientID
));
if (Config.Logging.DebugMessages)