Migrate to System.Text.Json in Live2D exporter

- [exp3.json] Fixed format of Blend values (there should be strings according to the official specification)
- Fixed BlendType enum
This commit is contained in:
VaDiM
2026-06-19 21:11:55 +03:00
parent 92602fc4a9
commit 7b7907f2eb
14 changed files with 188 additions and 105 deletions
@@ -9,7 +9,6 @@
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup Condition=" !$(TargetFramework.Contains('windows')) ">
@@ -2,8 +2,8 @@
{
public enum BlendType
{
Overwrite,
Add,
Multiply,
Overwrite,
}
}
@@ -1,4 +1,6 @@
namespace CubismLive2DExtractor
using System.Text.Json.Serialization;
namespace CubismLive2DExtractor
{
public class CubismExpression3Json
{
@@ -11,6 +13,7 @@
{
public string Id { get; set; }
public float Value { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public BlendType Blend { get; set; }
}
}
@@ -0,0 +1,25 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CubismLive2DExtractor.CubismJsonConverters
{
public sealed class MyJsonConverterBool : JsonConverter<bool>
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch(reader.TokenType)
{
case JsonTokenType.True: return true;
case JsonTokenType.False: return false;
case JsonTokenType.Number: return reader.GetByte() == 1;
default: throw new JsonException($"Unable to convert \"{reader.TokenType}\" token to boolean.");
}
}
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
}
@@ -0,0 +1,51 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CubismLive2DExtractor.CubismJsonConverters
{
public class MyJsonConverterFloat : JsonConverter<float>
{
private readonly bool _asDecimal;
public MyJsonConverterFloat(bool asDecimal = false)
{
_asDecimal = asDecimal;
}
public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<float>(ref reader, new JsonSerializerOptions
{
NumberHandling = options.NumberHandling
});
}
public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options)
{
if (float.IsNaN(value) || float.IsInfinity(value))
{
if (options.NumberHandling == JsonNumberHandling.AllowNamedFloatingPointLiterals)
{
writer.WriteStringValue($"{value.ToString(CultureInfo.InvariantCulture)}");
}
else
{
writer.WriteStringValue(JsonSerializer.Serialize(value));
}
}
else
{
if (_asDecimal && value.Equals((int)value))
{
writer.WriteNumberValue((decimal)value + 0.0m);
}
else
{
writer.WriteNumberValue(MathF.Round(value, 3));
}
}
}
}
}
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CubismLive2DExtractor.CubismJsonConverters
{
public class MyJsonConverterFloatList : JsonConverter<List<float>>
{
public override List<float> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<List<float>>(ref reader, new JsonSerializerOptions
{
NumberHandling = options.NumberHandling
});
}
public override void Write(Utf8JsonWriter writer, List<float> value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var n in value)
{
if (float.IsNaN(n) || float.IsInfinity(n))
{
if (options.NumberHandling == JsonNumberHandling.AllowNamedFloatingPointLiterals)
{
writer.WriteStringValue($"{n.ToString(CultureInfo.InvariantCulture)}");
}
else
{
writer.WriteStringValue(JsonSerializer.Serialize(value));
}
}
else
{
writer.WriteNumberValue(MathF.Round(n, 3));
}
}
writer.WriteEndArray();
}
}
}
@@ -1,4 +1,4 @@
using Newtonsoft.Json.Linq;
using System.Text.Json.Nodes;
namespace CubismLive2DExtractor
{
@@ -16,8 +16,8 @@ namespace CubismLive2DExtractor
public string Physics;
public string Pose;
public string DisplayInfo;
public JObject Motions;
public JArray Expressions;
public JsonObject Motions;
public JsonArray Expressions;
}
public class SerializableGroup
@@ -1,10 +1,12 @@
// File Format Specifications
// https://github.com/Live2D/CubismSpecs/blob/master/FileFormats/motion3.json.md
using AssetStudio;
using CubismLive2DExtractor.CubismJsonConverters;
using System;
using System.Collections.Generic;
using System.Linq;
using AssetStudio;
using System.Text.Json.Serialization;
namespace CubismLive2DExtractor
{
@@ -36,6 +38,7 @@ namespace CubismLive2DExtractor
public string Id;
public float FadeInTime;
public float FadeOutTime;
[JsonConverter(typeof(MyJsonConverterFloatList))]
public List<float> Segments;
}
@@ -1,14 +1,18 @@
using System;
using AssetStudio;
using CubismLive2DExtractor.CubismJsonConverters;
using CubismLive2DExtractor.CubismUnityClasses;
using System;
using System.Collections.Specialized;
using System.Linq;
using AssetStudio;
using CubismLive2DExtractor.CubismUnityClasses;
using Newtonsoft.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CubismLive2DExtractor
{
public static class CubismParsers
{
public static readonly JsonSerializerOptions CubismJsonOptions;
public enum CubismMonoBehaviourType
{
FadeController,
@@ -24,9 +28,21 @@ namespace CubismLive2DExtractor
RenderTexture,
}
static CubismParsers()
{
CubismJsonOptions = new JsonSerializerOptions
{
Converters = { new MyJsonConverterFloat(asDecimal: true) },
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = true,
IncludeFields = true,
WriteIndented = true,
};
}
public static string ParsePhysics(OrderedDictionary physicsDict, float motionFps)
{
var cubismPhysicsRig = JsonConvert.DeserializeObject<CubismPhysics>(JsonConvert.SerializeObject(physicsDict)).Rig;
var cubismPhysicsRig = JsonSerializer.Deserialize<CubismPhysics>(JsonSerializer.SerializeToUtf8Bytes(physicsDict, CubismJsonOptions), CubismJsonOptions).Rig;
var physicsSettings = new CubismPhysics3Json.SerializablePhysicsSettings[cubismPhysicsRig.SubRigs.Length];
for (var i = 0; i < physicsSettings.Length; i++)
@@ -129,7 +145,10 @@ namespace CubismLive2DExtractor
},
PhysicsSettings = physicsSettings
};
return JsonConvert.SerializeObject(physicsJson, Formatting.Indented, new MyJsonConverter2());
var physicsOptions = new JsonSerializerOptions(CubismJsonOptions);
physicsOptions.Converters.Clear();
physicsOptions.Converters.Add(new MyJsonConverterFloat());
return JsonSerializer.Serialize(physicsJson, physicsOptions);
}
public static OrderedDictionary ParseMonoBehaviour(MonoBehaviour m_MonoBehaviour, CubismMonoBehaviourType cubismMonoBehaviourType, AssemblyLoader assemblyLoader)
@@ -1,5 +1,5 @@
using System;
using AssetStudio;
using AssetStudio;
using System;
namespace CubismLive2DExtractor.CubismUnityClasses
{
@@ -1,5 +1,6 @@
using AssetStudio;
using Newtonsoft.Json;
using CubismLive2DExtractor.CubismJsonConverters;
using System.Text.Json.Serialization;
namespace CubismLive2DExtractor.CubismUnityClasses
{
@@ -33,6 +34,7 @@ namespace CubismLive2DExtractor.CubismUnityClasses
public float AngleScale { get; set; }
public float Weight { get; set; }
public CubismPhysicsSourceComponent SourceComponent { get; set; }
[JsonConverter(typeof(MyJsonConverterBool))]
public bool IsInverted { get; set; }
}
@@ -50,6 +52,7 @@ namespace CubismLive2DExtractor.CubismUnityClasses
public float AngleScale { get; set; }
public float Weight { get; set; }
public CubismPhysicsSourceComponent SourceComponent { get; set; }
[JsonConverter(typeof(MyJsonConverterBool))]
public bool IsInverted { get; set; }
}
@@ -71,7 +74,7 @@ namespace CubismLive2DExtractor.CubismUnityClasses
public sealed class CubismPhysics : MonoBehaviour
{
[JsonProperty("_rig")]
[JsonPropertyName("_rig")]
public CubismPhysicsRig Rig { get; set; }
}
}
@@ -6,8 +6,6 @@
using AssetStudio;
using AssetStudio.Addons.CubismLive2D;
using CubismLive2DExtractor.CubismUnityClasses;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -15,6 +13,8 @@ using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using static CubismLive2DExtractor.CubismParsers;
using Object = AssetStudio.Object;
@@ -315,7 +315,7 @@ namespace CubismLive2DExtractor
#endregion
#region motion3.json
var motions = new SortedDictionary<string, JArray>();
var motions = new SortedDictionary<string, JsonNode>();
var destMotionPath = Path.Combine(destPath, "motions") + Path.DirectorySeparatorChar;
var motionFps = 0f;
@@ -327,7 +327,7 @@ namespace CubismLive2DExtractor
var fadeMotionLstDict = ParseMonoBehaviour(FadeMotionLst, CubismMonoBehaviourType.FadeMotionList, Assembly);
if (fadeMotionLstDict != null)
{
var cubismFadeList = JsonConvert.DeserializeObject<CubismFadeMotionList>(JsonConvert.SerializeObject(fadeMotionLstDict));
var cubismFadeList = JsonSerializer.Deserialize<CubismFadeMotionList>(JsonSerializer.SerializeToUtf8Bytes(fadeMotionLstDict, CubismJsonOptions), CubismJsonOptions);
var fadeMotionAssetSet = new HashSet<MonoBehaviour>();
foreach (var motionPPtr in cubismFadeList.CubismFadeMotionObjects)
{
@@ -389,7 +389,7 @@ namespace CubismLive2DExtractor
#endregion
#region exp3.json
var expressions = new JArray();
var expressions = new JsonArray();
var destExpressionPath = Path.Combine(destPath, "expressions") + Path.DirectorySeparatorChar;
if (ExpressionLst != null) //Expressions from Expression List
@@ -398,7 +398,7 @@ namespace CubismLive2DExtractor
var expLstDict = ParseMonoBehaviour(ExpressionLst, CubismMonoBehaviourType.ExpressionList, Assembly);
if (expLstDict != null)
{
var cubismExpList = JsonConvert.DeserializeObject<CubismExpressionList>(JsonConvert.SerializeObject(expLstDict));
var cubismExpList = JsonSerializer.Deserialize<CubismExpressionList>(JsonSerializer.SerializeToUtf8Bytes(expLstDict, CubismJsonOptions), CubismJsonOptions);
var expAssetSet = new HashSet<MonoBehaviour>();
foreach (var expPPtr in cubismExpList.CubismExpressionObjects)
{
@@ -427,14 +427,14 @@ namespace CubismLive2DExtractor
if (expressionDict == null)
continue;
var expression = JsonConvert.DeserializeObject<CubismExpression3Json>(JsonConvert.SerializeObject(expressionDict));
var expression = JsonSerializer.Deserialize<CubismExpression3Json>(JsonSerializer.SerializeToUtf8Bytes(expressionDict, CubismJsonOptions), CubismJsonOptions);
expressions.Add(new JObject
expressions.Add(new JsonObject
{
{ "Name", expressionName },
{ "File", $"expressions/{expressionName}.exp3.json" }
["Name"] = expressionName,
["File"] = $"expressions/{expressionName}.exp3.json",
});
File.WriteAllText($"{destExpressionPath}{expressionName}.exp3.json", JsonConvert.SerializeObject(expression, Formatting.Indented));
File.WriteAllBytes($"{destExpressionPath}{expressionName}.exp3.json", JsonSerializer.SerializeToUtf8Bytes(expression, CubismJsonOptions));
}
#endregion
@@ -519,16 +519,16 @@ namespace CubismLive2DExtractor
Physics = isPhysicsExported ? $"{modelName}.physics3.json" : null,
Pose = isPoseExported ? $"{modelName}.pose3.json" : null,
DisplayInfo = isCdiExported ? $"{modelName}.cdi3.json" : null,
Motions = JObject.FromObject(motions),
Motions = new JsonObject(motions),
Expressions = expressions,
},
Groups = groups.ToArray()
};
File.WriteAllText($"{destPath}{modelName}.model3.json", JsonConvert.SerializeObject(model3, Formatting.Indented));
File.WriteAllBytes($"{destPath}{modelName}.model3.json", JsonSerializer.SerializeToUtf8Bytes(model3, CubismJsonOptions));
#endregion
}
private void ExportFadeMotions(string destMotionPath, SortedDictionary<string, JArray> motions, ref float fps)
private void ExportFadeMotions(string destMotionPath, SortedDictionary<string, JsonNode> motions, ref float fps)
{
Directory.CreateDirectory(destMotionPath);
foreach (var fadeMotionMono in FadeMotions)
@@ -536,8 +536,8 @@ namespace CubismLive2DExtractor
var fadeMotionDict = ParseMonoBehaviour(fadeMotionMono, CubismMonoBehaviourType.FadeMotion, Assembly);
if (fadeMotionDict == null)
continue;
var fadeMotion = JsonConvert.DeserializeObject<CubismFadeMotionData>(JsonConvert.SerializeObject(fadeMotionDict));
var fadeMotion = JsonSerializer.Deserialize<CubismFadeMotionData>(JsonSerializer.SerializeToUtf8Bytes(fadeMotionDict, CubismJsonOptions), CubismJsonOptions);
if (fadeMotion.ParameterIds.Length == 0)
continue;
@@ -551,13 +551,13 @@ namespace CubismLive2DExtractor
if (motions.ContainsKey(animName))
continue;
}
var motionPath = new JObject(new JProperty("File", $"motions/{animName}.motion3.json"));
motions.Add(animName, new JArray(motionPath));
File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(motionJson, Formatting.Indented, new MyJsonConverter()));
var motionPath = new JsonObject { ["File"] = $"motions/{animName}.motion3.json" };
motions.Add(animName, new JsonArray(motionPath));
File.WriteAllBytes($"{destMotionPath}{animName}.motion3.json", JsonSerializer.SerializeToUtf8Bytes(motionJson, CubismJsonOptions));
}
}
private void ExportClipMotions(string destMotionPath, CubismMotion3Converter converter, SortedDictionary<string, JArray> motions, ref float fps)
private void ExportClipMotions(string destMotionPath, CubismMotion3Converter converter, SortedDictionary<string, JsonNode> motions, ref float fps)
{
if (converter == null)
return;
@@ -584,9 +584,9 @@ namespace CubismLive2DExtractor
if (motions.ContainsKey(animName))
continue;
}
var motionPath = new JObject(new JProperty("File", $"motions/{animName}.motion3.json"));
motions.Add(animName, new JArray(motionPath));
File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(motionJson, Formatting.Indented, new MyJsonConverter()));
var motionPath = new JsonObject { ["File"] = $"motions/{animName}.motion3.json" };
motions.Add(animName, new JsonArray(motionPath));
File.WriteAllBytes($"{destMotionPath}{animName}.motion3.json", JsonSerializer.SerializeToUtf8Bytes(motionJson, CubismJsonOptions));
}
}
@@ -631,7 +631,7 @@ namespace CubismLive2DExtractor
{
poseJson.Groups[i++] = nodeList.ToArray();
}
File.WriteAllText($"{destPath}{modelName}.pose3.json", JsonConvert.SerializeObject(poseJson, Formatting.Indented));
File.WriteAllBytes($"{destPath}{modelName}.pose3.json", JsonSerializer.SerializeToUtf8Bytes(poseJson, CubismJsonOptions));
return true;
}
@@ -681,7 +681,7 @@ namespace CubismLive2DExtractor
if (parts.Count == 0 && parameters.Count == 0)
return false;
File.WriteAllText($"{destPath}{modelName}.cdi3.json", JsonConvert.SerializeObject(cdiJson, Formatting.Indented));
File.WriteAllBytes($"{destPath}{modelName}.cdi3.json", JsonSerializer.SerializeToUtf8Bytes(cdiJson, CubismJsonOptions));
return true;
}
@@ -1,35 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace CubismLive2DExtractor
{
public class MyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<float>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartArray();
Convert(writer, (List<float>)value);
writer.WriteEndArray();
}
private void Convert(JsonWriter writer, List<float> array)
{
foreach (var n in array)
{
var v = n.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
writer.WriteRawValue(v);
}
}
}
}
@@ -1,28 +0,0 @@
using System;
using Newtonsoft.Json;
namespace CubismLive2DExtractor
{
public class MyJsonConverter2 : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(float);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Convert(writer, (float)value);
}
private void Convert(JsonWriter writer, float value)
{
writer.WriteRawValue(value.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture));
}
}
}