874 lines
48 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|