Skip to content

Instantly share code, notes, and snippets.

@Lachee
Last active March 25, 2024 08:25
Show Gist options
  • Save Lachee/5f80fb5cb2be99dad9fc1ae5915d8263 to your computer and use it in GitHub Desktop.
Save Lachee/5f80fb5cb2be99dad9fc1ae5915d8263 to your computer and use it in GitHub Desktop.
Script that can read and parse Unity YAML files into basic trees.
using Lachee.Utilities.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lachee.Utilities.Serialization
{
public interface IUPropertyCollection
{
bool Add(UProperty property);
}
/// <summary>Basic data structure</summary>
public class UNode { }
/// <summary>Stores a key pair</summary>
public class UProperty
{
public string name;
public UNode value;
}
/// <summary>Stores a single value</summary>
public class UValue : UNode
{
public string value = "";
public UValue() { }
public UValue(string value)
{
this.value = value;
}
public override string ToString()
=> value;
}
/// <summary>Stores an array of values/objects</summary>
public class UArray : UNode, IUPropertyCollection
{
public List<UNode> items = new List<UNode>();
public bool Add(UProperty property)
=> Add(property.value);
public bool Add(UNode node)
{
items.Add(node);
return true;
}
}
/// <summary>Stores a map of names and properties</summary>
public class UObject : UNode, IUPropertyCollection
{
public Dictionary<string, UProperty> properties = new Dictionary<string, UProperty>();
public bool Add(UProperty property)
=> properties.TryAdd(property.name, property);
}
/// <summary>A single asset/class</summary>
public sealed class UComponent : UNode, IUPropertyCollection
{
public long fileID = -1;
public UClassID classID = UClassID.Object;
public string TypeName => _property?.name;
public UObject Component => (UObject)_property?.value;
public UProperty Property => _property;
private UProperty _property;
public bool Add(UProperty property)
{
if (!string.IsNullOrEmpty(TypeName))
return false;
_property = property;
return true;
}
public override string ToString()
=> $"{classID} &{fileID}";
}
public enum UClassID
{
Object = 0,
GameObject = 1,
Component = 2,
LevelGameManager = 3,
Transform = 4,
TimeManager = 5,
GlobalGameManager = 6,
Behaviour = 8,
GameManager = 9,
AudioManager = 11,
InputManager = 13,
EditorExtension = 18,
Physics2DSettings = 19,
Camera = 20,
Material = 21,
MeshRenderer = 23,
Renderer = 25,
Texture = 27,
Texture2D = 28,
OcclusionCullingSettings = 29,
GraphicsSettings = 30,
MeshFilter = 33,
OcclusionPortal = 41,
Mesh = 43,
Skybox = 45,
QualitySettings = 47,
Shader = 48,
TextAsset = 49,
Rigidbody2D = 50,
Collider2D = 53,
Rigidbody = 54,
PhysicsManager = 55,
Collider = 56,
Joint = 57,
CircleCollider2D = 58,
HingeJoint = 59,
PolygonCollider2D = 60,
BoxCollider2D = 61,
PhysicsMaterial2D = 62,
MeshCollider = 64,
BoxCollider = 65,
CompositeCollider2D = 66,
EdgeCollider2D = 68,
CapsuleCollider2D = 70,
ComputeShader = 72,
AnimationClip = 74,
ConstantForce = 75,
TagManager = 78,
AudioListener = 81,
AudioSource = 82,
AudioClip = 83,
RenderTexture = 84,
CustomRenderTexture = 86,
Cubemap = 89,
Avatar = 90,
AnimatorController = 91,
RuntimeAnimatorController = 93,
ScriptMapper = 94,
Animator = 95,
TrailRenderer = 96,
DelayedCallManager = 98,
TextMesh = 102,
RenderSettings = 104,
Light = 108,
CGProgram = 109,
BaseAnimationTrack = 110,
Animation = 111,
MonoBehaviour = 114,
MonoScript = 115,
MonoManager = 116,
Texture3D = 117,
NewAnimationTrack = 118,
Projector = 119,
LineRenderer = 120,
Flare = 121,
Halo = 122,
LensFlare = 123,
FlareLayer = 124,
HaloLayer = 125,
NavMeshProjectSettings = 126,
Font = 128,
PlayerSettings = 129,
NamedObject = 130,
PhysicMaterial = 134,
SphereCollider = 135,
CapsuleCollider = 136,
SkinnedMeshRenderer = 137,
FixedJoint = 138,
BuildSettings = 141,
AssetBundle = 142,
CharacterController = 143,
CharacterJoint = 144,
SpringJoint = 145,
WheelCollider = 146,
ResourceManager = 147,
PreloadData = 150,
ConfigurableJoint = 153,
TerrainCollider = 154,
TerrainData = 156,
LightmapSettings = 157,
WebCamTexture = 158,
EditorSettings = 159,
EditorUserSettings = 162,
AudioReverbFilter = 164,
AudioHighPassFilter = 165,
AudioChorusFilter = 166,
AudioReverbZone = 167,
AudioEchoFilter = 168,
AudioLowPassFilter = 169,
AudioDistortionFilter = 170,
SparseTexture = 171,
AudioBehaviour = 180,
AudioFilter = 181,
WindZone = 182,
Cloth = 183,
SubstanceArchive = 184,
ProceduralMaterial = 185,
ProceduralTexture = 186,
Texture2DArray = 187,
CubemapArray = 188,
OffMeshLink = 191,
OcclusionArea = 192,
Tree = 193,
NavMeshAgent = 195,
NavMeshSettings = 196,
ParticleSystem = 198,
ParticleSystemRenderer = 199,
ShaderVariantCollection = 200,
LODGroup = 205,
BlendTree = 206,
Motion = 207,
NavMeshObstacle = 208,
SortingGroup = 210,
SpriteRenderer = 212,
Sprite = 213,
CachedSpriteAtlas = 214,
ReflectionProbe = 215,
Terrain = 218,
LightProbeGroup = 220,
AnimatorOverrideController = 221,
CanvasRenderer = 222,
Canvas = 223,
RectTransform = 224,
CanvasGroup = 225,
BillboardAsset = 226,
BillboardRenderer = 227,
SpeedTreeWindAsset = 228,
AnchoredJoint2D = 229,
Joint2D = 230,
SpringJoint2D = 231,
DistanceJoint2D = 232,
HingeJoint2D = 233,
SliderJoint2D = 234,
WheelJoint2D = 235,
ClusterInputManager = 236,
BaseVideoTexture = 237,
NavMeshData = 238,
AudioMixer = 240,
AudioMixerController = 241,
AudioMixerGroupController = 243,
AudioMixerEffectController = 244,
AudioMixerSnapshotController = 245,
PhysicsUpdateBehaviour2D = 246,
ConstantForce2D = 247,
Effector2D = 248,
AreaEffector2D = 249,
PointEffector2D = 250,
PlatformEffector2D = 251,
SurfaceEffector2D = 252,
BuoyancyEffector2D = 253,
RelativeJoint2D = 254,
FixedJoint2D = 255,
FrictionJoint2D = 256,
TargetJoint2D = 257,
LightProbes = 258,
LightProbeProxyVolume = 259,
SampleClip = 271,
AudioMixerSnapshot = 272,
AudioMixerGroup = 273,
AssetBundleManifest = 290,
RuntimeInitializeOnLoadManager = 300,
UnityConnectSettings = 310,
AvatarMask = 319,
PlayableDirector = 320,
VideoPlayer = 328,
VideoClip = 329,
ParticleSystemForceField = 330,
SpriteMask = 331,
WorldAnchor = 362,
OcclusionCullingData = 363,
PrefabInstance = 1001,
EditorExtensionImpl = 1002,
AssetImporter = 1003,
AssetDatabaseV1 = 1004,
Mesh3DSImporter = 1005,
TextureImporter = 1006,
ShaderImporter = 1007,
ComputeShaderImporter = 1008,
AudioImporter = 1020,
HierarchyState = 1026,
AssetMetaData = 1028,
DefaultAsset = 1029,
DefaultImporter = 1030,
TextScriptImporter = 1031,
SceneAsset = 1032,
NativeFormatImporter = 1034,
MonoImporter = 1035,
LibraryAssetImporter = 1038,
ModelImporter = 1040,
FBXImporter = 1041,
TrueTypeFontImporter = 1042,
EditorBuildSettings = 1045,
InspectorExpandedState = 1048,
AnnotationManager = 1049,
PluginImporter = 1050,
EditorUserBuildSettings = 1051,
IHVImageFormatImporter = 1055,
AnimatorStateTransition = 1101,
AnimatorState = 1102,
HumanTemplate = 1105,
AnimatorStateMachine = 1107,
PreviewAnimationClip = 1108,
AnimatorTransition = 1109,
SpeedTreeImporter = 1110,
AnimatorTransitionBase = 1111,
SubstanceImporter = 1112,
LightmapParameters = 1113,
LightingDataAsset = 1120,
SketchUpImporter = 1124,
BuildReport = 1125,
PackedAssets = 1126,
VideoClipImporter = 1127,
Int = 100000,
Bool = 100001,
Float = 100002,
MonoObject = 100003,
Collision = 100004,
Vector3f = 100005,
RootMotionData = 100006,
Collision2D = 100007,
AudioMixerLiveUpdateFloat = 100008,
AudioMixerLiveUpdateBool = 100009,
Polygon2D = 100010,
Void = 100011,
TilemapCollider2D = 19719996,
AssetImporterLog = 41386430,
VFXRenderer = 73398921,
SerializableManagedRefTestClass = 76251197,
Grid = 156049354,
ScenesUsingAssets = 156483287,
ArticulationBody = 171741748,
Preset = 181963792,
EmptyObject = 277625683,
IConstraint = 285090594,
TestObjectWithSpecialLayoutOne = 293259124,
AssemblyDefinitionReferenceImporter = 294290339,
SiblingDerived = 334799969,
TestObjectWithSerializedMapStringNonAlignedStruct = 342846651,
SubDerived = 367388927,
AssetImportInProgressProxy = 369655926,
PluginBuildInfo = 382020655,
EditorProjectAccess = 426301858,
PrefabImporter = 468431735,
TestObjectWithSerializedArray = 478637458,
TestObjectWithSerializedAnimationCurve = 478637459,
TilemapRenderer = 483693784,
ScriptableCamera = 488575907,
SpriteAtlasAsset = 612988286,
SpriteAtlasDatabase = 638013454,
AudioBuildInfo = 641289076,
CachedSpriteAtlasRuntimeData = 644342135,
RendererFake = 646504946,
AssemblyDefinitionReferenceAsset = 662584278,
BuiltAssetBundleInfoSet = 668709126,
SpriteAtlas = 687078895,
RayTracingShaderImporter = 747330370,
RayTracingShader = 825902497,
LightingSettings = 850595691,
PlatformModuleSetup = 877146078,
VersionControlSettings = 890905787,
AimConstraint = 895512359,
VFXManager = 937362698,
VisualEffectSubgraph = 994735392,
VisualEffectSubgraphOperator = 994735403,
VisualEffectSubgraphBlock = 994735404,
Prefab = 1001480554,
LocalizationImporter = 1027052791,
Derived = 1091556383,
PropertyModificationsTargetTestObject = 1111377672,
ReferencesArtifactGenerator = 1114811875,
AssemblyDefinitionAsset = 1152215463,
SceneVisibilityState = 1154873562,
LookAtConstraint = 1183024399,
SpriteAtlasImporter = 1210832254,
MultiArtifactTestImporter = 1223240404,
GameObjectRecorder = 1268269756,
LightingDataAssetParent = 1325145578,
PresetManager = 1386491679,
TestObjectWithSpecialLayoutTwo = 1392443030,
StreamingManager = 1403656975,
LowerResBlitTexture = 1480428607,
StreamingController = 1542919678,
TestObjectVectorPairStringBool = 1628831178,
GridLayout = 1742807556,
AssemblyDefinitionImporter = 1766753193,
ParentConstraint = 1773428102,
FakeComponent = 1803986026,
PositionConstraint = 1818360608,
RotationConstraint = 1818360609,
ScaleConstraint = 1818360610,
Tilemap = 1839735485,
PackageManifest = 1896753125,
PackageManifestImporter = 1896753126,
TerrainLayer = 1953259897,
SpriteShapeRenderer = 1971053207,
NativeObjectType = 1977754360,
TestObjectWithSerializedMapStringBool = 1981279845,
SerializableManagedHost = 1995898324,
VisualEffectAsset = 2058629509,
VisualEffectImporter = 2058629510,
VisualEffectResource = 2058629511,
VisualEffectObject = 2059678085,
VisualEffect = 2083052967,
LocalizationAsset = 2083778819,
ScriptedImporter = 2089858483,
}
}
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
namespace Lachee.Utilities.Serialization
{
/// <summary>
/// Simple Parser for Unity YAML files. Able to produce basic tree structures, it is suitable for raw manipulation of the data but not serialization.
/// </summary>
public sealed class UYAMLParser
{
internal const string COMPONENT_HEADER = "--- !u!";
private IUPropertyCollection _curObject;
private UProperty _curProperty;
private Stack<IUPropertyCollection> _objects;
private int _spt;
private int _indentLevel;
private int _prevIndentLevel;
private UYAMLParser()
{
_spt = 0;
_objects = new Stack<IUPropertyCollection>();
Reset();
}
/// <summary>Resets the state of the parser</summary>
private void Reset()
{
_curObject = null;
_curProperty = null;
_objects.Clear();
_indentLevel = 0;
_prevIndentLevel = 0;
}
/// <summary>Parses the given UYAML content</summary>
public static List<UComponent> Parse(string content)
{
int offset;
int nextOffset;
string block;
// Get to first chunk
offset = content.IndexOf(COMPONENT_HEADER);
if (offset == -1)
throw new System.InvalidOperationException("There was no blocks found");
List<UComponent> components = new List<UComponent>();
UYAMLParser parser = new UYAMLParser();
do
{
nextOffset = content.IndexOf(COMPONENT_HEADER, offset + COMPONENT_HEADER.Length);
if (nextOffset == -1)
block = content.Substring(offset);
else
block = content.Substring(offset, nextOffset - offset-1);
var component = parser.ParseComponent(block);
components.Add(component);
offset = nextOffset;
} while (offset >= 0);
return components;
}
private UComponent ParseComponent(string content)
{
Reset();
int offset;
int nextOffset;
string line;
// Get to the first line
offset = content.IndexOf(COMPONENT_HEADER);
if (offset == -1)
throw new System.InvalidOperationException("The block is missing the content header");
do
{
nextOffset = content.IndexOf('\n', offset)+1;
if (nextOffset <= 0) line = content.Substring(offset);
else line = content.Substring(offset, nextOffset - offset -1);
ParseLine(line);
offset = nextOffset;
} while (offset > 0);
while (_objects.TryPop(out var node))
{
if (node is UComponent comp)
return comp;
}
return null;
}
private void ParseLine(string content)
{
if (string.IsNullOrWhiteSpace(content))
return;
string line;
bool isArrayEntry = false;
_prevIndentLevel = _indentLevel;
_indentLevel = Tabulate(content, out line);
if (_spt < 1) // Update the indentation level based of the first item we meet
{
_spt = _indentLevel;
_indentLevel = Tabulate(content, out line);
}
// No block yet, expecting a new def
if (_curObject == null)
{
if (!line.StartsWith(COMPONENT_HEADER))
throw new ParseException($"Expecting a new block, but got '{line}' instead.");
string[] segs = line.Split(' ');
if (segs.Length != 3)
throw new ParseException("Expecting 3 parts in the block header.");
_curObject = new UComponent()
{
classID = (UClassID) int.Parse(segs[1].Substring(3)),
fileID = long.Parse(segs[2].Substring(1))
};
return;
}
// Start of Array
if (line[0] == '-')
{
line = line.Trim('-', '\t', ' ');
_indentLevel++;
isArrayEntry = true;
}
// Start of Object
int indentDiff = _indentLevel - _prevIndentLevel;
if (indentDiff == 1) // Something wrong with this logic when adding item,s to array
{
if (isArrayEntry)
{
// We have increased our indentation and we have started with a -,
// that means we starting an array and the parent property needs to be converted
// into an array.
// We need to convert the current property to an array
var arr = new UArray();
_objects.Push(_curObject);
_curProperty.value = arr;
_curObject = arr;
}
else
{
// Our indentation level has increased so we are making a new object
var obj = new UObject();
_objects.Push(_curObject);
_curProperty.value = obj;
_curObject = obj;
}
}
else if (indentDiff < 0)
{
while (_objects.Count > _indentLevel)
_curObject = _objects.Pop();
}
else if (indentDiff != 0)
{
throw new ParseException("Indentation grew/shrunk too rapidly. Expected only a change of either 0 or 1.");
}
// Seperate the parts
string[] parts = line.Split(':', 2);
if (parts.Length == 1)
{
if (_curObject is UArray arr)
arr.Add(ParseValue(line));
else
throw new ParseException("Cannot add key-less values to an object");
}
else
{
_curProperty = new UProperty();
if (parts.Length != 2)
throw new ParseException($"Cannot find property name in '{line}'");
_curProperty.name = parts[0].Trim();
_curProperty.value = ParseValue(parts[1]);
if (isArrayEntry)
{
if (_curObject is not UArray && _objects.Peek() is UArray)
_curObject = _objects.Pop();
if (_curObject is UArray arr)
{
// Create a new object to put this property into
var obj = new UObject();
_objects.Push(_curObject);
_curObject = obj;
arr.Add(obj);
}
else
{
throw new ParseException("Adding a new array item, but could not get an array to put it in.");
}
}
// Attempt to push the current property into the current object
if (_curProperty != null && !_curObject.Add(_curProperty))
throw new ParseException($"Failed to add the property '{_curProperty.name}'");
}
#if false
// If we are in the middle of an array, we need to either add to the
// array or build the items
if (_curObject is UArray curArray)
{
// Seperate the parts
bool isProperty = line.IndexOf(':') > 0;
bool isInlineShape = line[0] == '{' || line[0] == '[';
if (!isProperty && !isInlineShape) // A single value, we will push it to the array directly
{
if (!isArrayEntry)
throw new ParseException("Array Values must begin with -");
curArray.Add(ParseValue(line));
}
else if (isInlineShape)
{
if (!isArrayEntry)
throw new ParseException("Array Values must begin with -");
curArray.Add(ParseValue(line));
}
else // An object for a value
{
if (isArrayEntry) // Its a new item, so create the item and push it to the array.
{
var item = new UObject();
curArray.Add(item);
}
string[] parts = line.Split(':');
if (parts.Length == 2) // If we have a name: value, add it to the last array item.
{
var lastItem = curArray.items[curArray.items.Count - 1];
if (lastItem is IUPropertyCollection propertyCollection)
{
var property = new UProperty()
{
name = parts[0].Trim(),
value = ParseValue(parts[1])
};
if (!propertyCollection.Add(property))
throw new ParseException($"Duplicate property found '{property.name}'");
}
else
{
throw new ParseException("Previous array item was not a property collection. Cannot add new properties.");
}
}
}
}
else // Adding a property to the previous object.
{
// Seperate the parts
string[] parts = line.Split(':', 2);
_curProperty = new UProperty();
if (parts.Length != 2)
throw new ParseException($"Cannot find property name in '{line}'");
_curProperty.name = parts[0].Trim();
_curProperty.value = ParseValue(parts[1]);
// Attempt to push the current property into the current object
if (_curProperty != null && !_curObject.Add(_curProperty))
throw new ParseException($"Duplicate property found '{_curProperty.name}'");
}
#endif
}
private UNode ParseValue(string value)
{
string[] parts;
string content = value.Trim();
if (content.Length == 0)
return new UValue();
if (content[0] == '{' && content[content.Length - 1] == '}') {
UObject objValue = new UObject();
if (content.Length > 2)
{
parts = content.Split(',');
foreach (var part in parts)
{
string[] sParts = part.Trim('{', '}', ' ').Split(':', 2);
if (sParts.Length != 2)
throw new ParseException("Cannot parse non-key values inside a inline object!");
objValue.Add(new UProperty()
{
name = sParts[0].Trim(),
value = ParseValue(sParts[1])
});
}
}
return objValue;
}
if (content[0] == '[' && content[content.Length - 1] == ']') {
UArray arrayValue = new UArray();
if (content.Length > 2)
{
parts = content.Split(',');
foreach (var part in parts)
arrayValue.Add(new UValue(part.Trim('[', ']', ' ')));
}
return arrayValue;
}
if (content[0] == '"' && content[content.Length - 1] == '"')
content = content.Trim('"').Replace("\\\"", "\"");
return new UValue(content);
}
private int Tabulate(string content, out string line)
{
int spaces;
for (spaces = 0; spaces < content.Length; spaces++)
{
if (content[spaces] != ' ' && content[spaces] != '\t')
break;
}
line = content.TrimStart(' ', '\t', '\n', '\r');
return spaces / Math.Max(_spt, 1);
}
}
/// <summary>Represents errors that occure during parsing</summary>
public sealed class ParseException : System.Exception
{
public ParseException(string message) : base(message) { }
}
}
using System.Collections.Generic;
using System.Text;
namespace Lachee.Utilities.Serialization
{
public class UYAMLWriter
{
private const string DEFAULT_EOL = "\r\n";
private StringBuilder builder = new StringBuilder();
private string eol = DEFAULT_EOL;
public int IndentationSpaces { get; set; } = 2;
public bool InlineObjects { get; set; } = true;
public bool InlineComplexObjects { get; set; } = false;
public bool InlineArrays { get; set; } = true;
public bool InlineComplexArrays { get; set; } = false;
public void AddComponets(IEnumerable<UComponent> components)
{
foreach (var component in components)
AddComponent(component);
}
public void AddComponent(UComponent component)
{
builder.Append(UYAMLParser.COMPONENT_HEADER)
.Append((int)component.classID)
.Append(" &")
.Append(component.fileID)
.Append(eol);
AppendProperty(component.Property, 0, false, false);
}
private void AppendProperty(UProperty property, int indent, bool arrayItem, bool skipIndent)
{
int indentLevel = indent;
int indentSize = IndentationSpaces;
string name = string.IsNullOrEmpty(property.name) ? "" : property.name + ":";
if (arrayItem)
{
name = "-";
indentLevel -= 1;
}
if (skipIndent)
indentSize = 0;
switch (property.value)
{
default: break;
case UValue uValue:
builder.Append(' ', indentLevel * indentSize).Append(name).Append(" ").Append(uValue.value);
builder.Append(eol);
break;
case UObject uObject:
if (CanInline(uObject) && !arrayItem)
{
builder.Append(' ', indentLevel * indentSize).Append(name).Append(" {");
eol = "";
bool first = true;
foreach (var kp in uObject.properties)
{
if (!first) builder.Append(", ");
AppendProperty(kp.Value, 0, false, true);
first = false;
}
eol = DEFAULT_EOL;
builder.Append('}').Append(eol);
}
else
{
bool first = true;
builder.Append(' ', indentLevel * indentSize).Append(name);
builder.Append(arrayItem ? ' ' : eol); // This inlines the first time of an array
foreach (var kp in uObject.properties)
{
AppendProperty(kp.Value, indentLevel + 1, false, arrayItem && first);
first = false;
}
}
break;
case UArray uArray:
if (CanInline(uArray))
{
builder.Append(' ', indentLevel * indentSize).Append(name).Append(" [");
eol = "";
bool first = true;
for (int i = 0; i < uArray.items.Count; i++)
{
if (!first) builder.Append(",");
AppendProperty(new UProperty() { value = uArray.items[i] }, 0, false, true);
first = false;
}
eol = DEFAULT_EOL;
builder.Append(']').Append(eol);
}
else
{
builder.Append(' ', indentLevel * indentSize).Append(name);
builder.Append(eol);
for (int i = 0; i < uArray.items.Count; i++)
AppendProperty(new UProperty() { value = uArray.items[i] }, indentLevel + 1, true, false);
}
break;
}
}
private bool CanInline(UObject obj)
{
if (!InlineObjects)
return false;
if (obj.properties.Count == 0)
return true;
foreach(var kp in obj.properties)
{
if (InlineComplexObjects)
{
if (kp.Value.value is UObject o && !CanInline(o))
return false;
if (kp.Value.value is UArray a && !CanInline(a))
return false;
}
else
{
if (kp.Value.value is not UValue)
return false;
}
}
return true;
}
private bool CanInline(UArray arr)
{
if (!InlineArrays)
return false;
if (arr.items.Count == 0)
return true;
foreach(var item in arr.items)
{
if (InlineComplexArrays)
{
if (item is UObject o && !CanInline(o))
return false;
if (item is UArray a && !CanInline(a))
return false;
}
else
{
if (item is not UValue)
return false;
}
}
return true;
}
public override string ToString()
=> $"%YAML 1.1{DEFAULT_EOL}%TAG !u! tag:unity3d.com,2011:{DEFAULT_EOL}" + builder.ToString();
}
}
@Lachee
Copy link
Author

Lachee commented Mar 25, 2024

License is MIT. I provide no warranty or liability for the code.

@Lachee
Copy link
Author

Lachee commented Mar 25, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment