using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.CodeDom; using System.CodeDom.Compiler; using System.Text.RegularExpressions; using UnityEngine; using UnityEditor; using UnityEditor.Compilation; namespace Normal.Realtime.Serialization { [CustomEditor(typeof(MonoScript))] public class ModelEditor : Editor { private MonoScript _script { get { return (MonoScript)target; } } private const string _codeBlockHeader = "/* ----- Begin Normal Autogenerated Code ----- */"; private const string _codeBlockFooter = "/* ----- End Normal Autogenerated Code ----- */"; public override void OnInspectorGUI() { Type scriptClass = _script.GetClass(); RealtimeModelAttribute realtimeModelAttribute = GetRealtimeModelAttribute(scriptClass); if (realtimeModelAttribute != null) { // If this is a model file. Do some cool UI for it. if (GUILayout.Button("Compile Model")) { try { string modelCode = GenerateModel(); SaveGeneratedModel(modelCode); } catch (Exception exception) { Debug.LogError("Failed to compile model."); Debug.LogException(exception); } } if (GUILayout.Button("Clear Autogenerated Model")) { try { SaveGeneratedModel(null); } catch (Exception exception) { Debug.LogException(exception); Debug.LogError("Failed to clear model."); } } } else { DrawDefaultInspectorGUI(); } } private static RealtimeModelAttribute GetRealtimeModelAttribute(Type type) { if (type == null) return null; object[] customAttributes = type.GetCustomAttributes(typeof(RealtimeModelAttribute), false); return customAttributes.FirstOrDefault() as RealtimeModelAttribute; } private string GenerateModel() { // Model Type modelClass = _script.GetClass(); string classNamespace = modelClass.Namespace; string className = modelClass.Name; if (modelClass.BaseType != typeof(System.Object)) throw new ArgumentException("RealtimeModel is currently a subclass of '" + modelClass.BaseType + "'. RealtimeModels cannot have a superclass."); bool hasMetaModel = false; RealtimeModelAttribute realtimeModelAttribute = GetRealtimeModelAttribute(modelClass); if (realtimeModelAttribute != null) { hasMetaModel = realtimeModelAttribute.createMetaModel; } // Properties Dictionary allProperties; Dictionary reliableProperties; Dictionary unreliableProperties; Dictionary modelProperties; Dictionary eventProperties; GetRealtimePropertyAttributesForModel(modelClass, out allProperties, out reliableProperties, out unreliableProperties, out modelProperties, out eventProperties); // Model configuration bool hasNamespace = classNamespace != null; bool hasChangeCache = reliableProperties.Count > 0; // Model code List code = new List(); int level = 0; // Namespace if (hasNamespace) code.Add(Indent(level++) + "namespace " + classNamespace + " {"); // Class code.Add(Indent(level++) + "public partial class " + className + " : IModel {"); // Public properties if (allProperties.Count > 0) { code.Add(Indent(level) + "// Properties"); foreach (KeyValuePair property in allProperties) { uint propertyID = property.Key; FieldInfo field = property.Value; if (reliableProperties.ContainsKey(propertyID)) { // Reliable code.Add(Indent(level++) + "public " + GetTypeAsString(field.FieldType) + " " + GetCleanPropertyName(field.Name, false) + " {"); code.Add(Indent(level) + "get { return _cache.LookForValueInCache(" + field.Name + ", entry => entry." + GetCleanPropertyName(field.Name, false) + "Set, entry => entry." + GetCleanPropertyName(field.Name, false) + "); }"); code.Add(Indent(level) + "set { if (value == " + GetCleanPropertyName(field.Name, false) + ") return; _cache.UpdateLocalCache(entry => { entry." + GetCleanPropertyName(field.Name, false) + "Set = true; entry." + GetCleanPropertyName(field.Name, false) + " = value; return entry; });" + (eventProperties.ContainsKey(propertyID) ? " Fire" + GetCleanPropertyName(field.Name, true) + "DidChange(value);" : "") + " }"); code.Add(Indent(--level) + "}"); } else if (unreliableProperties.ContainsKey(propertyID)) { // Unreliable code.Add(Indent(level++) + "public " + GetTypeAsString(field.FieldType) + " " + GetCleanPropertyName(field.Name, false) + " {"); code.Add(Indent(level) + "get { return " + field.Name + "; }"); code.Add(Indent(level) + "set { if (value == " + field.Name + ") return; _" + GetCleanPropertyName(field.Name, false) + "ShouldWrite = true; " + field.Name + " = value;" + (eventProperties.ContainsKey(propertyID) ? " Fire" + GetCleanPropertyName(field.Name, true) + "DidChange(value);" : "") + " }"); code.Add(Indent(--level) + "}"); } else if (modelProperties.ContainsKey(propertyID)) { // Model code.Add(Indent(level++) + "public " + GetTypeAsString(field.FieldType) + " " + GetCleanPropertyName(field.Name, false) + " {"); code.Add(Indent(level) + "get { return " + field.Name + "; }"); code.Add(Indent(--level) + "}"); } } code.Add(Indent(level)); } // Events if (eventProperties.Count > 0) { code.Add(Indent(level) + "// Events"); foreach (KeyValuePair property in eventProperties) { FieldInfo field = property.Value; code.Add(Indent(level) + "public delegate void " + GetCleanPropertyName(field.Name, true) + "DidChange(" + _script.GetClass().Name + " model, " + GetTypeAsString(field.FieldType) + " value);"); code.Add(Indent(level) + "public event " + GetCleanPropertyName(field.Name, true) + "DidChange " + GetCleanPropertyName(field.Name, false) + "DidChange;"); } code.Add(Indent(level)); } // Ownership if (hasMetaModel) { code.Add(Indent(level) + "// Ownership"); code.Add(Indent(level) + "public int ownerID { get { return _metaModel.ownerID; } }"); code.Add(Indent(level)); code.Add(Indent(level++) + "public void RequestOwnership(int clientIndex) {"); code.Add(Indent(level) + "_metaModel.ownerID = clientIndex;"); code.Add(Indent(--level) + "}"); code.Add(Indent(level)); code.Add(Indent(level++) + "public void ClearOwnership() {"); code.Add(Indent(level) + "_metaModel.ownerID = -1;"); code.Add(Indent(--level) + "}"); code.Add(Indent(level)); } // Meta model if (hasMetaModel) { code.Add(Indent(level) + "// Meta model"); code.Add(Indent(level) + "private MetaModel _metaModel;"); code.Add(Indent(level)); } // Change cache if (hasChangeCache) { code.Add(Indent(level) + "// Delta updates"); code.Add(Indent(level++) + "private struct LocalCacheEntry {"); int longestTypeLength = 4; // bool foreach (KeyValuePair property in reliableProperties) { FieldInfo field = property.Value; int typeLength = GetTypeAsString(field.FieldType).Length; if (typeLength > longestTypeLength) longestTypeLength = typeLength; } foreach (KeyValuePair property in reliableProperties) { FieldInfo field = property.Value; string typeName = GetTypeAsString(field.FieldType); code.Add(Indent(level) + "public bool " + Space(longestTypeLength-4) + GetCleanPropertyName(field.Name, false) + "Set;"); code.Add(Indent(level) + "public " + typeName + Space(longestTypeLength-typeName.Length) + " " + GetCleanPropertyName(field.Name, false) + ";"); } code.Add(Indent(--level) + "}"); code.Add(Indent(level)); code.Add(Indent(level) + "private LocalChangeCache _cache;"); code.Add(Indent(level)); } // Unreliable property should write bools if (unreliableProperties.Count > 0) { foreach (KeyValuePair property in unreliableProperties) { FieldInfo field = property.Value; code.Add(Indent(level) + "private bool _" + GetCleanPropertyName(field.Name, false) + "ShouldWrite;"); } code.Add(Indent(level)); } // Constructor code.Add(Indent(level++) + "public " + className + "() {"); if (hasMetaModel) { code.Add(Indent(level) + "_metaModel = new MetaModel();"); } if (hasChangeCache) { code.Add(Indent(level) + "_cache = new LocalChangeCache();"); } if (hasChangeCache && modelProperties.Count > 0) code.Add(Indent(level)); if (modelProperties.Count > 0) { foreach (KeyValuePair property in modelProperties) { FieldInfo field = property.Value; code.Add(Indent(level) + field.Name + " = new " + GetTypeAsString(field.FieldType) + "();"); } } code.Add(Indent(--level) + "}"); code.Add(Indent(level)); // Events if (eventProperties.Count > 0) { code.Add(Indent(level) + "// Events"); foreach (KeyValuePair property in eventProperties) { FieldInfo field = property.Value; code.Add(Indent(level++) + "public void Fire" + GetCleanPropertyName(field.Name, true) + "DidChange(" + GetTypeAsString(field.FieldType) + " value) {"); code.Add(Indent(level++) + "try {"); code.Add(Indent(level++) + "if (" + GetCleanPropertyName(field.Name, false) + "DidChange != null)"); code.Add(Indent(level--) + GetCleanPropertyName(field.Name, false) + "DidChange(this, value);"); code.Add(Indent(level-1) + "} catch (System.Exception exception) {"); code.Add(Indent(level) + "Debug.LogException(exception);"); code.Add(Indent(--level) + "}"); code.Add(Indent(--level) + "}"); } code.Add(Indent(level)); } //// Serialization // PropertyID enum code.Add(Indent(level) + "// Serialization"); code.Add(Indent(level++) + "enum PropertyID {"); foreach (KeyValuePair property in allProperties) { uint propertyID = property.Key; FieldInfo field = property.Value; code.Add(Indent(level) + GetCleanPropertyName(field.Name, true) + " = " + propertyID + ","); } code.Add(Indent(--level) + "}"); code.Add(Indent(level)); // Write Length code.Add(Indent(level++) + "public int WriteLength(StreamContext context) {"); code.Add(Indent(level) + "int length = 0;"); code.Add(Indent(level)); code.Add(Indent(level++) + "if (context.fullModel) {"); if (hasChangeCache) { code.Add(Indent(level) + "// Mark unreliable properties as clean and flatten the in-flight cache."); code.Add(Indent(level) + "// TODO: Move this out of WriteLength() once we have a prepareToWrite method."); foreach (KeyValuePair property in unreliableProperties) { FieldInfo field = property.Value; code.Add(Indent(level) + "_" + GetCleanPropertyName(field.Name, false) + "ShouldWrite = false;"); } foreach (KeyValuePair property in reliableProperties) { FieldInfo field = property.Value; code.Add(Indent(level) + field.Name + " = " + GetCleanPropertyName(field.Name, false) + ";"); } code.Add(Indent(level) + "_cache.Clear();"); code.Add(Indent(level)); } if (hasMetaModel) { code.Add(Indent(level) + "// Meta model"); code.Add(Indent(level) + "length += WriteStream.WriteModelLength(0, _metaModel, context);"); code.Add(Indent(level)); } code.Add(Indent(level) + "// Write all properties"); foreach (KeyValuePair property in allProperties) { uint propertyID = property.Key; FieldInfo field = property.Value; code.Add(Indent(level) + GetWriteLengthLineForProperty(field)); } code.Add(Indent(level-1) + "} else {"); // Meta model if (hasMetaModel) { code.Add(Indent(level) + "// Meta model"); code.Add(Indent(level) + "length += WriteStream.WriteModelLength(0, _metaModel, context);"); } if (hasMetaModel && unreliableProperties.Count > 0) code.Add(Indent(level)); // Unreliable properties if (unreliableProperties.Count > 0) { code.Add(Indent(level) + "// Unreliable properties"); code.Add(Indent(level++) + "if (context.unreliableChannel) {"); foreach (KeyValuePair property in unreliableProperties) { FieldInfo field = property.Value; code.Add(Indent(level++) + "if (_" + GetCleanPropertyName(field.Name, false) + "ShouldWrite) {"); code.Add(Indent(level) + GetWriteLengthLineForProperty(field)); code.Add(Indent(--level) + "}"); } code.Add(Indent(--level) + "}"); } if (unreliableProperties.Count > 0 && reliableProperties.Count > 0) code.Add(Indent(level)); // Reliable properties if (reliableProperties.Count > 0) { code.Add(Indent(level) + "// Reliable properties"); code.Add(Indent(level++) + "if (context.reliableChannel) {"); if (hasChangeCache) { code.Add(Indent(level) + "LocalCacheEntry entry = _cache.localCache;"); } foreach (KeyValuePair property in reliableProperties) { FieldInfo field = property.Value; code.Add(Indent(level++) + "if (entry." + GetCleanPropertyName(field.Name, false) + "Set)"); code.Add(Indent(level--) + GetWriteLengthLineForProperty(field, true)); } code.Add(Indent(--level) + "}"); } if (reliableProperties.Count > 0 && modelProperties.Count > 0) code.Add(Indent(level)); // Models if (modelProperties.Count > 0) { code.Add(Indent(level) + "// Models"); foreach (KeyValuePair property in modelProperties) { FieldInfo field = property.Value; code.Add(Indent(level) + GetWriteLengthLineForProperty(field, false)); } } code.Add(Indent(--level) + "}"); code.Add(Indent(level)); code.Add(Indent(level) + "return length;"); code.Add(Indent(--level) + "}"); code.Add(Indent(level)); // Write code.Add(Indent(level++) + "public void Write(WriteStream stream, StreamContext context) {"); code.Add(Indent(level++) + "if (context.fullModel) {"); if (hasMetaModel) { code.Add(Indent(level) + "// Meta model"); code.Add(Indent(level) + "stream.WriteModel(0, _metaModel, context);"); code.Add(Indent(level)); } code.Add(Indent(level) + "// Write all properties"); foreach (KeyValuePair property in allProperties) { uint propertyID = property.Key; FieldInfo field = property.Value; bool unreliableProperty = unreliableProperties.ContainsKey(propertyID); code.Add(Indent(level) + GetWriteLineForProperty(field)); if (unreliableProperty) code.Add(Indent(level) + "_" + GetCleanPropertyName(field.Name, false) + "ShouldWrite = false;"); } code.Add(Indent(level-1) + "} else {"); // Meta model if (hasMetaModel) { code.Add(Indent(level) + "// Meta model"); code.Add(Indent(level) + "stream.WriteModel(0, _metaModel, context);"); } if (hasMetaModel && unreliableProperties.Count > 0) code.Add(Indent(level)); // Unreliable properties if (unreliableProperties.Count > 0) { code.Add(Indent(level) + "// Unreliable properties"); code.Add(Indent(level++) + "if (context.unreliableChannel) {"); foreach (KeyValuePair property in unreliableProperties) { FieldInfo field = property.Value; code.Add(Indent(level++) + "if (_" + GetCleanPropertyName(field.Name, false) + "ShouldWrite) {"); code.Add(Indent(level) + GetWriteLineForProperty(field)); code.Add(Indent(level) + "_" + GetCleanPropertyName(field.Name, false) + "ShouldWrite = false;"); code.Add(Indent(--level) + "}"); } code.Add(Indent(--level) + "}"); } if (unreliableProperties.Count > 0 && reliableProperties.Count > 0) code.Add(Indent(level)); // Reliable properties if (reliableProperties.Count > 0) { code.Add(Indent(level) + "// Reliable properties"); code.Add(Indent(level++) + "if (context.reliableChannel) {"); if (hasChangeCache) { code.Add(Indent(level) + "LocalCacheEntry entry = _cache.localCache;"); code.Add(Indent(level++) + GetIfPushToChangeCacheLineForProperties(reliableProperties.Values)); code.Add(Indent(level--) + "_cache.PushLocalCacheToInflight(context.updateID);"); code.Add(Indent(level)); } foreach (KeyValuePair property in reliableProperties) { FieldInfo field = property.Value; code.Add(Indent(level++) + "if (entry." + GetCleanPropertyName(field.Name, false) + "Set)"); code.Add(Indent(level--) + GetWriteLineForProperty(field, true)); } code.Add(Indent(--level) + "}"); } if (reliableProperties.Count > 0 && modelProperties.Count > 0) code.Add(Indent(level)); // Models if (modelProperties.Count > 0) { code.Add(Indent(level) + "// Models"); foreach (KeyValuePair property in modelProperties) { FieldInfo field = property.Value; code.Add(Indent(level) + GetWriteLineForProperty(field, false)); } } code.Add(Indent(--level) + "}"); code.Add(Indent(--level) + "}"); code.Add(Indent(level)); // Read code.Add(Indent(level++) + "public void Read(ReadStream stream, StreamContext context) {"); if (hasChangeCache) { IEnumerable> reliableEventProperties = reliableProperties.Intersect(eventProperties); if (reliableEventProperties.Count() > 0) { foreach (KeyValuePair property in reliableEventProperties) { uint propertyID = property.Key; FieldInfo field = property.Value; code.Add(Indent(level) + "bool " + GetCleanPropertyName(field.Name, false) + "ExistsInChangeCache = _cache.ValueExistsInCache(entry => entry." + GetCleanPropertyName(field.Name, false) + "Set);"); } code.Add(Indent(level)); } code.Add(Indent(level) + "// Remove from in-flight"); code.Add(Indent(level++) + "if (context.deltaUpdatesOnly && context.reliableChannel)"); code.Add(Indent(level--) + "_cache.RemoveUpdateFromInflight(context.updateID);"); code.Add(Indent(level)); } code.Add(Indent(level) + "// Loop through each property and deserialize"); code.Add(Indent(level) + "uint propertyID;"); code.Add(Indent(level++) + "while (stream.ReadNextPropertyID(out propertyID)) {"); code.Add(Indent(level++) + "switch (propertyID) {"); if (hasMetaModel) { code.Add(Indent(level++) + "case 0:"); code.Add(Indent(level) + "// Meta model"); code.Add(Indent(level) + "stream.ReadModel(_metaModel, context);"); code.Add(Indent(level--) + "break;"); } foreach (KeyValuePair property in allProperties) { uint propertyID = property.Key; FieldInfo field = property.Value; bool reliableProperty = reliableProperties.ContainsKey(propertyID); bool unreliableProperty = unreliableProperties.ContainsKey(propertyID); bool eventProperty = eventProperties.ContainsKey(propertyID); code.Add(Indent(level++) + "case (uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ": {"); if (eventProperty) { code.Add(Indent(level) + GetTypeAsString(field.FieldType) + " previousValue = " + field.Name + ";"); code.Add(Indent(level)); } code.Add(Indent(level) + GetReadLineForProperty(field)); if (unreliableProperty) { code.Add(Indent(level) + "_" + GetCleanPropertyName(field.Name, false) + "ShouldWrite = false;"); } if (eventProperty) { code.Add(Indent(level)); code.Add(Indent(level++) + "if (" + (reliableProperty ? "!" + GetCleanPropertyName(field.Name, false) + "ExistsInChangeCache && " : "") + field.Name + " != previousValue)"); code.Add(Indent(level--) + "Fire" + GetCleanPropertyName(field.Name, true) + "DidChange(" + field.Name + ");"); } code.Add(Indent(level) + "break;"); code.Add(Indent(--level) + "}"); } code.Add(Indent(level++) + "default:"); code.Add(Indent(level) + "stream.SkipProperty();"); code.Add(Indent(level--) + "break;"); code.Add(Indent(--level) + "}"); code.Add(Indent(--level) + "}"); code.Add(Indent(--level) + "}"); // Class code.Add(Indent(--level) + "}"); // Namespace if (hasNamespace) code.Add(Indent(--level) + "}"); // Convert to string and return string codeString = ""; foreach (string line in code) codeString += line + Environment.NewLine; return codeString; } private static string Indent(int level) { string indent = ""; for (int i = 0; i < level; i++) indent += " "; return indent; } private static string Space(int level) { string indent = ""; for (int i = 0; i < level; i++) indent += " "; return indent; } private static string GetTypeAsString(Type type) { CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("C#"); CodeTypeReference typeReference = new CodeTypeReference(type); CodeTypeReferenceExpression typeReferenceExpression = new CodeTypeReferenceExpression(typeReference); using (StringWriter typeStringWriter = new StringWriter()) { codeDomProvider.GenerateCodeFromExpression(typeReferenceExpression, typeStringWriter, new CodeGeneratorOptions()); return typeStringWriter.GetStringBuilder().ToString(); } } private static string GetCleanPropertyName(string propertyString, bool capitalizeFirstLetter = false) { // Remove leading _ if it exists. if (propertyString[0] == '_') propertyString = propertyString.Substring(1); char[] propertyName = propertyString.ToCharArray(); // Capitalize the first letter if (capitalizeFirstLetter) propertyName[0] = char.ToUpperInvariant(propertyName[0]); return new string(propertyName); } private static void GetRealtimePropertyAttributesForModel(Type modelClass, out Dictionary allProperties, out Dictionary reliableProperties, out Dictionary unreliableProperties, out Dictionary modelProperties, out Dictionary eventProperties) { allProperties = new Dictionary(); reliableProperties = new Dictionary(); unreliableProperties = new Dictionary(); modelProperties = new Dictionary(); eventProperties = new Dictionary(); // Get all fields with the RealtimeProperty attribute IEnumerable fields = modelClass.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Where(property => Attribute.IsDefined(property, typeof(RealtimePropertyAttribute))); // Group them into reliable/unreliable foreach (FieldInfo field in fields) { RealtimePropertyAttribute realtimePropertyAttribute = field.GetCustomAttributes(typeof(RealtimePropertyAttribute), true).FirstOrDefault() as RealtimePropertyAttribute; uint propertyID = realtimePropertyAttribute.propertyID; // Verify the property ID is valid if (propertyID == 0) throw new ArgumentException("RealtimeProperty must have a property ID greater than 0. (Field: " + field.Name + ")"); // Check upper bounds (it's not uint max because of the wireType bits) if (propertyID > Serialization.PropertyIDMaxValue) throw new ArgumentException("RealtimeProperty must have a property ID less than or equal to " + Serialization.PropertyIDMaxValue + ". (Field: " + field.Name + ")"); // Verify that the property ID is unique FieldInfo duplicatePropertyID; if (allProperties.TryGetValue(propertyID, out duplicatePropertyID)) throw new ArgumentException("Models cannot contain duplicate property IDs. (Field: " + field.Name + ", " + duplicatePropertyID.Name + ")"); // Verify that field name starts with an underscore and lowercase letter if (field.Name[0] != '_' || field.Name[1] != char.ToLowerInvariant(field.Name[1])) throw new ArgumentException("Field name must start with an underscore and a lowercase letter (ie _myVariable). (Field: " + field.Name + ")"); // Store allProperties[propertyID] = field; if (field.FieldType.GetInterface(typeof(IModel).FullName) != null) { modelProperties[propertyID] = field; } else { if (realtimePropertyAttribute.reliable) reliableProperties[propertyID] = field; else unreliableProperties[propertyID] = field; if (realtimePropertyAttribute.createDidChangeEvent) eventProperties[propertyID] = field; } } } private static string GetIfPushToChangeCacheLineForProperties(IEnumerable fields) { string line = "if ("; bool hasFieldBefore = false; foreach (FieldInfo field in fields) { if (hasFieldBefore) line += " || "; line += "entry." + GetCleanPropertyName(field.Name, false) + "Set"; hasFieldBefore = true; } line += ")"; return line; } private static string GetWriteLengthLineForProperty(FieldInfo field, bool entry = false) { Type type = field.FieldType; TypeCode typeCode = Type.GetTypeCode(type); string variableName = field.Name; if (entry) variableName = "entry." + GetCleanPropertyName(field.Name, false); switch (typeCode) { case TypeCode.Boolean: return "length += WriteStream.WriteVarint32Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + " ? 1u : 0u);"; case TypeCode.SByte: return "length += WriteStream.WriteVarint32Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", (uint)" + variableName + ");"; case TypeCode.Byte: return "length += WriteStream.WriteVarint32Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Int16: return "length += WriteStream.WriteVarint32Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", (uint)" + variableName + ");"; case TypeCode.UInt16: return "length += WriteStream.WriteVarint32Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Int32: return "length += WriteStream.WriteVarint32Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", (uint)" + variableName + ");"; case TypeCode.UInt32: return "length += WriteStream.WriteVarint32Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Int64: return "length += WriteStream.WriteVarint64Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", (ulong)" + variableName + ");"; case TypeCode.UInt64: return "length += WriteStream.WriteVarint64Length((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Single: return "length += WriteStream.WriteFloatLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ");"; case TypeCode.Double: return "length += WriteStream.WriteDoubleLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ");"; case TypeCode.String: return "length += WriteStream.WriteStringLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Object: // Collection / Model if ((type.GetInterface(typeof(ICollection).FullName) != null)) return "length += WriteStream.WriteCollectionLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ", context);"; if ((type.GetInterface(typeof(IModel).FullName) != null)) return "length += WriteStream.WriteModelLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ", context);"; // Byte array if (type == typeof(byte[])) return "length += WriteStream.WriteBytesLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ".Length);"; // Unity primitives if (type == typeof(Color)) return "length += WriteStream.WriteBytesLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", WriteStream.ColorToBytesLength());"; if (type == typeof(Vector2)) return "length += WriteStream.WriteBytesLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", WriteStream.Vector2ToBytesLength());"; if (type == typeof(Vector3)) return "length += WriteStream.WriteBytesLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", WriteStream.Vector3ToBytesLength());"; if (type == typeof(Quaternion)) return "length += WriteStream.WriteBytesLength((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", WriteStream.QuaternionToBytesLength());"; break; } throw new ArgumentException("Unsupported serialization type: " + type + " (Field: " + field.Name + "). Property must be a primitive type or an object that implements the IModel interface."); } private static string GetWriteLineForProperty(FieldInfo field, bool entry = false) { Type type = field.FieldType; TypeCode typeCode = Type.GetTypeCode(type); string variableName = field.Name; if (entry) variableName = "entry." + GetCleanPropertyName(field.Name, false); switch (typeCode) { case TypeCode.Boolean: return "stream.WriteVarint32((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + " ? 1u : 0u);"; case TypeCode.SByte: return "stream.WriteVarint32((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", (uint)" + variableName + ");"; case TypeCode.Byte: return "stream.WriteVarint32((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Int16: return "stream.WriteVarint32((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", (uint)" + variableName + ");"; case TypeCode.UInt16: return "stream.WriteVarint32((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Int32: return "stream.WriteVarint32((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", (uint)" + variableName + ");"; case TypeCode.UInt32: return "stream.WriteVarint32((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Int64: return "stream.WriteVarint64((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", (ulong)" + variableName + ");"; case TypeCode.UInt64: return "stream.WriteVarint64((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Single: return "stream.WriteFloat((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Double: return "stream.WriteDouble((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.String: return "stream.WriteString((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; case TypeCode.Object: // Collection / Model if ((type.GetInterface(typeof(ICollection).FullName) != null)) return "stream.WriteCollection((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ", context);"; if ((type.GetInterface(typeof(IModel).FullName) != null)) return "stream.WriteModel((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ", context);"; // Byte array if (type == typeof(byte[])) return "stream.WriteBytes((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", " + variableName + ");"; // Unity primitives if (type == typeof(Color)) return "stream.WriteBytes((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", WriteStream.ColorToBytes(" + variableName + "));"; if (type == typeof(Vector2)) return "stream.WriteBytes((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", WriteStream.Vector2ToBytes(" + variableName + "));"; if (type == typeof(Vector3)) return "stream.WriteBytes((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", WriteStream.Vector3ToBytes(" + variableName + "));"; if (type == typeof(Quaternion)) return "stream.WriteBytes((uint)PropertyID." + GetCleanPropertyName(field.Name, true) + ", WriteStream.QuaternionToBytes(" + variableName + "));"; break; } throw new ArgumentException("Unsupported serialization type: " + type + " (Field: " + field.Name + "). Property must be a primitive type or an object that implements the IModel interface."); } private static string GetReadLineForProperty(FieldInfo field) { Type type = field.FieldType; TypeCode typeCode = Type.GetTypeCode(type); switch (typeCode) { case TypeCode.Boolean: return field.Name + " = (stream.ReadVarint32() != 0);"; case TypeCode.SByte: return field.Name + " = (sbyte)stream.ReadVarint32();"; case TypeCode.Byte: return field.Name + " = (byte)stream.ReadVarint32();"; case TypeCode.Int16: return field.Name + " = (short)stream.ReadVarint32();"; case TypeCode.UInt16: return field.Name + " = (ushort)stream.ReadVarint32();"; case TypeCode.Int32: return field.Name + " = (int)stream.ReadVarint32();"; case TypeCode.UInt32: return field.Name + " = stream.ReadVarint32();"; case TypeCode.Int64: return field.Name + " = (long)stream.ReadVarint64();"; case TypeCode.UInt64: return field.Name + " = stream.ReadVarint64();"; case TypeCode.Single: return field.Name + " = stream.ReadFloat();"; case TypeCode.Double: return field.Name + " = stream.ReadDouble();"; case TypeCode.String: return field.Name + " = stream.ReadString();"; case TypeCode.Object: // Collection / Model if ((type.GetInterface(typeof(ICollection).FullName) != null)) return "stream.ReadCollection(" + field.Name + ", context);"; if ((type.GetInterface(typeof(IModel).FullName) != null)) return "stream.ReadModel(" + field.Name + ", context);"; // Byte array if (type == typeof(byte[])) return field.Name + " = stream.ReadBytes();"; // Unity primitives if (type == typeof(Color)) return field.Name + " = ReadStream.ColorFromBytes(stream.ReadBytes());"; if (type == typeof(Vector2)) return field.Name + " = ReadStream.Vector2FromBytes(stream.ReadBytes());"; if (type == typeof(Vector3)) return field.Name + " = ReadStream.Vector3FromBytes(stream.ReadBytes());"; if (type == typeof(Quaternion)) return field.Name + " = ReadStream.QuaternionFromBytes(stream.ReadBytes());"; break; } throw new ArgumentException("Unsupported serialization type: " + type + " (Field: " + field.Name + "). Property must be a primitive type or an object that implements the IModel interface."); } private void SaveGeneratedModel(string modelCode) { string scriptPath = AssetDatabase.GetAssetPath(_script); // Read script StreamReader streamReader = new StreamReader(scriptPath); string scriptText = streamReader.ReadToEnd(); streamReader.Close(); streamReader.Dispose(); // Remove old autogenerated model code string codeBlockRegex = "\r?\n?" + Regex.Escape(_codeBlockHeader) + ".*" + Regex.Escape(_codeBlockFooter) + "\r?\n?"; scriptText = Regex.Replace(scriptText, codeBlockRegex, "", RegexOptions.Singleline); if (modelCode != null) { Type modelClass = _script.GetClass(); // Add "using Normal.Realtime.Serialization;" to the file header if needed. string usingNormalSerializationPattern = "^\\s*using\\s*" + Regex.Escape("Normal.Realtime.Serialization;") + "\\s*$"; Match usingNormalSerializationMatch = Regex.Match(scriptText, usingNormalSerializationPattern, RegexOptions.Multiline); if (!usingNormalSerializationMatch.Success) { string usingPattern = "^\\s*using\\s*.+;\\s*$"; Match usingMatch = Regex.Match(scriptText, usingPattern, RegexOptions.Multiline); int positionToPlaceUsingNormalSerialization = 0; while (usingMatch.Success) { positionToPlaceUsingNormalSerialization = usingMatch.Index + usingMatch.Length - 1; usingMatch = usingMatch.NextMatch(); } scriptText = scriptText.Insert(positionToPlaceUsingNormalSerialization, "using Normal.Realtime.Serialization;" + Environment.NewLine); } // Make sure the class definition is marked as partial scriptText = AddPartialToAllClassDefinitionsOfType(scriptText, modelClass); // Append new autogenerated model code scriptText += Environment.NewLine + _codeBlockHeader + Environment.NewLine + modelCode + _codeBlockFooter + Environment.NewLine; } // Write script StreamWriter streamWriter = new StreamWriter(scriptPath); streamWriter.Write(scriptText); streamWriter.Flush(); streamWriter.Close(); streamWriter.Dispose(); // Refresh asset database try { AssetDatabase.Refresh(ImportAssetOptions.DontDownloadFromCacheServer); } catch (Exception exception) { Debug.LogException(exception); } } private static string AddPartialToAllClassDefinitionsOfType(string scriptText, Type type) { string classDefinitionPattern = "^(.*)class\\s+" + type.Name + ".*$"; Match classDefinitionMatch = Regex.Match(scriptText, classDefinitionPattern, RegexOptions.Multiline); while (classDefinitionMatch.Success) { Group group = classDefinitionMatch.Groups[1]; string classDefinition = group.Value; if (!classDefinition.ToLowerInvariant().Contains("partial")) { // Add partial scriptText = scriptText.Insert(group.Index+group.Length, "partial "); // The rest of the Match objects are now invalid, so we will call this function again and bail from this run. return AddPartialToAllClassDefinitionsOfType(scriptText, type); } classDefinitionMatch = classDefinitionMatch.NextMatch(); } return scriptText; } private void DrawDefaultInspectorGUI() { if (base.targets.Length == 1) { string assetPath = AssetDatabase.GetAssetPath(base.target); string assemblyNameFromScriptPath = CompilationPipeline.GetAssemblyNameFromScriptPath(assetPath); if (assemblyNameFromScriptPath != null) { GUILayout.Label("Assembly Information", EditorStyles.boldLabel, new GUILayoutOption[0]); EditorGUILayout.LabelField("Filename", assemblyNameFromScriptPath, new GUILayoutOption[0]); string assemblyDefinitionFilePathFromScriptPath = CompilationPipeline.GetAssemblyDefinitionFilePathFromScriptPath(assetPath); if (assemblyDefinitionFilePathFromScriptPath != null) { TextAsset obj = AssetDatabase.LoadAssetAtPath(assemblyDefinitionFilePathFromScriptPath); using (new EditorGUI.DisabledScope(true)) { EditorGUILayout.ObjectField("Definition File", obj, typeof(TextAsset), false, new GUILayoutOption[0]); } } EditorGUILayout.Space(); } } bool enabled = GUI.enabled; GUI.enabled = true; TextAsset textAsset = base.target as TextAsset; if (textAsset != null) { string text = ""; if (base.targets.Length > 1) { //text = this.targetTitle; } else { text = textAsset.ToString(); if (text.Length > 7000) { text = text.Substring(0, 7000) + "...\n\n<...etc...>"; } } Rect rect = GUILayoutUtility.GetRect(new GUIContent(text), "ScriptText"); rect.x = 0f; rect.y -= 3f; rect.width = Screen.width - 10.0f; GUI.Box(rect, text, "ScriptText"); } GUI.enabled = enabled; } } }