holopy3/Assets/Normal/Realtime/Editor/RealtimeModelEditor.cs
2020-12-10 15:25:12 +01:00

874 lines
48 KiB
C#

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<uint, FieldInfo> allProperties;
Dictionary<uint, FieldInfo> reliableProperties;
Dictionary<uint, FieldInfo> unreliableProperties;
Dictionary<uint, FieldInfo> modelProperties;
Dictionary<uint, FieldInfo> 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<string> code = new List<string>();
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<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> property in reliableProperties) {
FieldInfo field = property.Value;
int typeLength = GetTypeAsString(field.FieldType).Length;
if (typeLength > longestTypeLength)
longestTypeLength = typeLength;
}
foreach (KeyValuePair<uint, FieldInfo> 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<LocalCacheEntry> _cache;");
code.Add(Indent(level));
}
// Unreliable property should write bools
if (unreliableProperties.Count > 0) {
foreach (KeyValuePair<uint, FieldInfo> 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<LocalCacheEntry>();");
}
if (hasChangeCache && modelProperties.Count > 0)
code.Add(Indent(level));
if (modelProperties.Count > 0) {
foreach (KeyValuePair<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> property in unreliableProperties) {
FieldInfo field = property.Value;
code.Add(Indent(level) + "_" + GetCleanPropertyName(field.Name, false) + "ShouldWrite = false;");
}
foreach (KeyValuePair<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> 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<KeyValuePair<uint, FieldInfo>> reliableEventProperties = reliableProperties.Intersect(eventProperties);
if (reliableEventProperties.Count() > 0) {
foreach (KeyValuePair<uint, FieldInfo> 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<uint, FieldInfo> 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<uint, FieldInfo> allProperties, out Dictionary<uint, FieldInfo> reliableProperties, out Dictionary<uint, FieldInfo> unreliableProperties, out Dictionary<uint, FieldInfo> modelProperties, out Dictionary<uint, FieldInfo> eventProperties) {
allProperties = new Dictionary<uint, FieldInfo>();
reliableProperties = new Dictionary<uint, FieldInfo>();
unreliableProperties = new Dictionary<uint, FieldInfo>();
modelProperties = new Dictionary<uint, FieldInfo>();
eventProperties = new Dictionary<uint, FieldInfo>();
// Get all fields with the RealtimeProperty attribute
IEnumerable<FieldInfo> 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<FieldInfo> 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<TextAsset>(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;
}
}
}