From 9ee17074fb20a307880206e051d58a67300473e4 Mon Sep 17 00:00:00 2001 From: Jacqui Manzi Date: Sun, 4 Dec 2016 22:39:16 -0800 Subject: [PATCH] ENG - Prototype end-to-end robot messages over TCP. --- Assets/Plugins/SimpleJSON.cs | 1070 +++++++++++++++++ Assets/Plugins/SimpleJSON.cs.meta | 12 + Assets/SerialComm/Scripts/PackageAssembler.cs | 68 ++ .../Scripts/PackageAssembler.cs.meta | 12 + Assets/SerialComm/Scripts/RobotCommand.cs | 58 + .../SerialComm/Scripts/RobotCommand.cs.meta | 12 + Assets/SerialComm/Scripts/RobotConnection.cs | 249 ++++ .../Scripts/RobotConnection.cs.meta | 12 + Assets/SerialComm/Scripts/RobotMessages.cs | 210 ++++ .../SerialComm/Scripts/RobotMessages.cs.meta | 12 + Assets/SerialComm/Scripts/TellyNetConnect.cs | 78 +- Assets/SerialComm/Scripts/Tokenizer.cs | 64 + Assets/SerialComm/Scripts/Tokenizer.cs.meta | 12 + ProjectSettings/ProjectSettings.asset | Bin 39209 -> 44357 bytes ProjectSettings/ProjectVersion.txt | 2 +- 15 files changed, 1831 insertions(+), 40 deletions(-) create mode 100644 Assets/Plugins/SimpleJSON.cs create mode 100644 Assets/Plugins/SimpleJSON.cs.meta create mode 100644 Assets/SerialComm/Scripts/PackageAssembler.cs create mode 100644 Assets/SerialComm/Scripts/PackageAssembler.cs.meta create mode 100644 Assets/SerialComm/Scripts/RobotCommand.cs create mode 100644 Assets/SerialComm/Scripts/RobotCommand.cs.meta create mode 100644 Assets/SerialComm/Scripts/RobotConnection.cs create mode 100644 Assets/SerialComm/Scripts/RobotConnection.cs.meta create mode 100644 Assets/SerialComm/Scripts/RobotMessages.cs create mode 100644 Assets/SerialComm/Scripts/RobotMessages.cs.meta create mode 100644 Assets/SerialComm/Scripts/Tokenizer.cs create mode 100644 Assets/SerialComm/Scripts/Tokenizer.cs.meta diff --git a/Assets/Plugins/SimpleJSON.cs b/Assets/Plugins/SimpleJSON.cs new file mode 100644 index 0000000..f4647a9 --- /dev/null +++ b/Assets/Plugins/SimpleJSON.cs @@ -0,0 +1,1070 @@ +//#define USE_SharpZipLib +#if !UNITY_WEBPLAYER +#define USE_FileIO +#endif + +/* * * * * + * A simple JSON Parser / builder + * ------------------------------ + * + * It mainly has been written as a simple JSON parser. It can build a JSON string + * from the node-tree, or generate a node tree from any valid JSON string. + * + * If you want to use compression when saving to file / stream / B64 you have to include + * SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) in your project and + * define "USE_SharpZipLib" at the top of the file + * + * Written by Bunny83 + * 2012-06-09 + * + * Features / attributes: + * - provides strongly typed node classes and lists / dictionaries + * - provides easy access to class members / array items / data values + * - the parser ignores data types. Each value is a string. + * - only double quotes (") are used for quoting strings. + * - values and names are not restricted to quoted strings. They simply add up and are trimmed. + * - There are only 3 types: arrays(JSONArray), objects(JSONClass) and values(JSONData) + * - provides "casting" properties to easily convert to / from those types: + * int / float / double / bool + * - provides a common interface for each node so no explicit casting is required. + * - the parser try to avoid errors, but if malformed JSON is parsed the result is undefined + * + * + * 2012-12-17 Update: + * - Added internal JSONLazyCreator class which simplifies the construction of a JSON tree + * Now you can simple reference any item that doesn't exist yet and it will return a JSONLazyCreator + * The class determines the required type by it's further use, creates the type and removes itself. + * - Added binary serialization / deserialization. + * - Added support for BZip2 zipped binary format. Requires the SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) + * The usage of the SharpZipLib library can be disabled by removing or commenting out the USE_SharpZipLib define at the top + * - The serializer uses different types when it comes to store the values. Since my data values + * are all of type string, the serializer will "try" which format fits best. The order is: int, float, double, bool, string. + * It's not the most efficient way but for a moderate amount of data it should work on all platforms. + * + * * * * */ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + + +namespace SimpleJSON +{ + public enum JSONBinaryTag + { + Array = 1, + Class = 2, + Value = 3, + IntValue = 4, + DoubleValue = 5, + BoolValue = 6, + FloatValue = 7, + } + + public class JSONNode + { + #region common interface + public virtual void Add(string aKey, JSONNode aItem){ } + public virtual JSONNode this[int aIndex] { get { return null; } set { } } + public virtual JSONNode this[string aKey] { get { return null; } set { } } + public virtual string Value { get { return ""; } set { } } + public virtual int Count { get { return 0; } } + + public virtual void Add(JSONNode aItem) + { + Add("", aItem); + } + + public virtual JSONNode Remove(string aKey) { return null; } + public virtual JSONNode Remove(int aIndex) { return null; } + public virtual JSONNode Remove(JSONNode aNode) { return aNode; } + + public virtual IEnumerable Childs { get { yield break;} } + public IEnumerable DeepChilds + { + get + { + foreach (var C in Childs) + foreach (var D in C.DeepChilds) + yield return D; + } + } + + public override string ToString() + { + return "JSONNode"; + } + public virtual string ToString(string aPrefix) + { + return "JSONNode"; + } + + #endregion common interface + + #region typecasting properties + public virtual int AsInt + { + get + { + int v = 0; + if (int.TryParse(Value,out v)) + return v; + return 0; + } + set + { + Value = value.ToString(); + } + } + public virtual float AsFloat + { + get + { + float v = 0.0f; + if (float.TryParse(Value,out v)) + return v; + return 0.0f; + } + set + { + Value = value.ToString(); + } + } + public virtual double AsDouble + { + get + { + double v = 0.0; + if (double.TryParse(Value,out v)) + return v; + return 0.0; + } + set + { + Value = value.ToString(); + } + } + public virtual bool AsBool + { + get + { + bool v = false; + if (bool.TryParse(Value,out v)) + return v; + return !string.IsNullOrEmpty(Value); + } + set + { + Value = (value)?"true":"false"; + } + } + public virtual JSONArray AsArray + { + get + { + return this as JSONArray; + } + } + public virtual JSONClass AsObject + { + get + { + return this as JSONClass; + } + } + + + #endregion typecasting properties + + #region operators + public static implicit operator JSONNode(string s) + { + return new JSONData(s); + } + public static implicit operator string(JSONNode d) + { + return (d == null)?null:d.Value; + } + public static bool operator ==(JSONNode a, object b) + { + if (b == null && a is JSONLazyCreator) + return true; + return System.Object.ReferenceEquals(a,b); + } + + public static bool operator !=(JSONNode a, object b) + { + return !(a == b); + } + public override bool Equals (object obj) + { + return System.Object.ReferenceEquals(this, obj); + } + public override int GetHashCode () + { + return base.GetHashCode(); + } + + + #endregion operators + + internal static string Escape(string aText) + { + string result = ""; + foreach(char c in aText) + { + switch(c) + { + case '\\' : result += "\\\\"; break; + case '\"' : result += "\\\""; break; + case '\n' : result += "\\n" ; break; + case '\r' : result += "\\r" ; break; + case '\t' : result += "\\t" ; break; + case '\b' : result += "\\b" ; break; + case '\f' : result += "\\f" ; break; + default : result += c ; break; + } + } + return result; + } + + public static JSONNode Parse(string aJSON) + { + Stack stack = new Stack(); + JSONNode ctx = null; + int i = 0; + string Token = ""; + string TokenName = ""; + bool QuoteMode = false; + while (i < aJSON.Length) + { + switch (aJSON[i]) + { + case '{': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + stack.Push(new JSONClass()); + if (ctx != null) + { + TokenName = TokenName.Trim(); + if (ctx is JSONArray) + ctx.Add(stack.Peek()); + else if (TokenName != "") + ctx.Add(TokenName,stack.Peek()); + } + TokenName = ""; + Token = ""; + ctx = stack.Peek(); + break; + + case '[': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + + stack.Push(new JSONArray()); + if (ctx != null) + { + TokenName = TokenName.Trim(); + if (ctx is JSONArray) + ctx.Add(stack.Peek()); + else if (TokenName != "") + ctx.Add(TokenName,stack.Peek()); + } + TokenName = ""; + Token = ""; + ctx = stack.Peek(); + break; + + case '}': + case ']': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + if (stack.Count == 0) + throw new Exception("JSON Parse: Too many closing brackets"); + + stack.Pop(); + if (Token != "") + { + TokenName = TokenName.Trim(); + if (ctx is JSONArray) + ctx.Add(Token); + else if (TokenName != "") + ctx.Add(TokenName,Token); + } + TokenName = ""; + Token = ""; + if (stack.Count>0) + ctx = stack.Peek(); + break; + + case ':': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + TokenName = Token; + Token = ""; + break; + + case '"': + QuoteMode ^= true; + break; + + case ',': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + if (Token != "") + { + if (ctx is JSONArray) + ctx.Add(Token); + else if (TokenName != "") + ctx.Add(TokenName, Token); + } + TokenName = ""; + Token = ""; + break; + + case '\r': + case '\n': + break; + + case ' ': + case '\t': + if (QuoteMode) + Token += aJSON[i]; + break; + + case '\\': + ++i; + if (QuoteMode) + { + char C = aJSON[i]; + switch (C) + { + case 't' : Token += '\t'; break; + case 'r' : Token += '\r'; break; + case 'n' : Token += '\n'; break; + case 'b' : Token += '\b'; break; + case 'f' : Token += '\f'; break; + case 'u': + { + string s = aJSON.Substring(i+1,4); + Token += (char)int.Parse(s, System.Globalization.NumberStyles.AllowHexSpecifier); + i += 4; + break; + } + default : Token += C; break; + } + } + break; + + default: + Token += aJSON[i]; + break; + } + ++i; + } + if (QuoteMode) + { + throw new Exception("JSON Parse: Quotation marks seems to be messed up."); + } + return ctx; + } + + public virtual void Serialize(System.IO.BinaryWriter aWriter) {} + + public void SaveToStream(System.IO.Stream aData) + { + var W = new System.IO.BinaryWriter(aData); + Serialize(W); + } + + #if USE_SharpZipLib + public void SaveToCompressedStream(System.IO.Stream aData) + { + using (var gzipOut = new ICSharpCode.SharpZipLib.BZip2.BZip2OutputStream(aData)) + { + gzipOut.IsStreamOwner = false; + SaveToStream(gzipOut); + gzipOut.Close(); + } + } + + public void SaveToCompressedFile(string aFileName) + { + #if USE_FileIO + System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName); + using(var F = System.IO.File.OpenWrite(aFileName)) + { + SaveToCompressedStream(F); + } + #else + throw new Exception("Can't use File IO stuff in webplayer"); + #endif + } + public string SaveToCompressedBase64() + { + using (var stream = new System.IO.MemoryStream()) + { + SaveToCompressedStream(stream); + stream.Position = 0; + return System.Convert.ToBase64String(stream.ToArray()); + } + } + + #else + public void SaveToCompressedStream(System.IO.Stream aData) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + public void SaveToCompressedFile(string aFileName) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + public string SaveToCompressedBase64() + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + #endif + + public void SaveToFile(string aFileName) + { + #if USE_FileIO + System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName); + using(var F = System.IO.File.OpenWrite(aFileName)) + { + SaveToStream(F); + } + #else + throw new Exception("Can't use File IO stuff in webplayer"); + #endif + } + public string SaveToBase64() + { + using (var stream = new System.IO.MemoryStream()) + { + SaveToStream(stream); + stream.Position = 0; + return System.Convert.ToBase64String(stream.ToArray()); + } + } + public static JSONNode Deserialize(System.IO.BinaryReader aReader) + { + JSONBinaryTag type = (JSONBinaryTag)aReader.ReadByte(); + switch(type) + { + case JSONBinaryTag.Array: + { + int count = aReader.ReadInt32(); + JSONArray tmp = new JSONArray(); + for(int i = 0; i < count; i++) + tmp.Add(Deserialize(aReader)); + return tmp; + } + case JSONBinaryTag.Class: + { + int count = aReader.ReadInt32(); + JSONClass tmp = new JSONClass(); + for(int i = 0; i < count; i++) + { + string key = aReader.ReadString(); + var val = Deserialize(aReader); + tmp.Add(key, val); + } + return tmp; + } + case JSONBinaryTag.Value: + { + return new JSONData(aReader.ReadString()); + } + case JSONBinaryTag.IntValue: + { + return new JSONData(aReader.ReadInt32()); + } + case JSONBinaryTag.DoubleValue: + { + return new JSONData(aReader.ReadDouble()); + } + case JSONBinaryTag.BoolValue: + { + return new JSONData(aReader.ReadBoolean()); + } + case JSONBinaryTag.FloatValue: + { + return new JSONData(aReader.ReadSingle()); + } + + default: + { + throw new Exception("Error deserializing JSON. Unknown tag: " + type); + } + } + } + + #if USE_SharpZipLib + public static JSONNode LoadFromCompressedStream(System.IO.Stream aData) + { + var zin = new ICSharpCode.SharpZipLib.BZip2.BZip2InputStream(aData); + return LoadFromStream(zin); + } + public static JSONNode LoadFromCompressedFile(string aFileName) + { + #if USE_FileIO + using(var F = System.IO.File.OpenRead(aFileName)) + { + return LoadFromCompressedStream(F); + } + #else + throw new Exception("Can't use File IO stuff in webplayer"); + #endif + } + public static JSONNode LoadFromCompressedBase64(string aBase64) + { + var tmp = System.Convert.FromBase64String(aBase64); + var stream = new System.IO.MemoryStream(tmp); + stream.Position = 0; + return LoadFromCompressedStream(stream); + } + #else + public static JSONNode LoadFromCompressedFile(string aFileName) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + public static JSONNode LoadFromCompressedStream(System.IO.Stream aData) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + public static JSONNode LoadFromCompressedBase64(string aBase64) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + #endif + + public static JSONNode LoadFromStream(System.IO.Stream aData) + { + using(var R = new System.IO.BinaryReader(aData)) + { + return Deserialize(R); + } + } + public static JSONNode LoadFromFile(string aFileName) + { + #if USE_FileIO + using(var F = System.IO.File.OpenRead(aFileName)) + { + return LoadFromStream(F); + } + #else + throw new Exception("Can't use File IO stuff in webplayer"); + #endif + } + public static JSONNode LoadFromBase64(string aBase64) + { + var tmp = System.Convert.FromBase64String(aBase64); + var stream = new System.IO.MemoryStream(tmp); + stream.Position = 0; + return LoadFromStream(stream); + } + } // End of JSONNode + + public class JSONArray : JSONNode, IEnumerable + { + private List m_List = new List(); + public override JSONNode this[int aIndex] + { + get + { + if (aIndex<0 || aIndex >= m_List.Count) + return new JSONLazyCreator(this); + return m_List[aIndex]; + } + set + { + if (aIndex<0 || aIndex >= m_List.Count) + m_List.Add(value); + else + m_List[aIndex] = value; + } + } + public override JSONNode this[string aKey] + { + get{ return new JSONLazyCreator(this);} + set{ m_List.Add(value); } + } + public override int Count + { + get { return m_List.Count; } + } + public override void Add(string aKey, JSONNode aItem) + { + m_List.Add(aItem); + } + public override JSONNode Remove(int aIndex) + { + if (aIndex < 0 || aIndex >= m_List.Count) + return null; + JSONNode tmp = m_List[aIndex]; + m_List.RemoveAt(aIndex); + return tmp; + } + public override JSONNode Remove(JSONNode aNode) + { + m_List.Remove(aNode); + return aNode; + } + public override IEnumerable Childs + { + get + { + foreach(JSONNode N in m_List) + yield return N; + } + } + public IEnumerator GetEnumerator() + { + foreach(JSONNode N in m_List) + yield return N; + } + public override string ToString() + { + string result = "[ "; + foreach (JSONNode N in m_List) + { + if (result.Length > 2) + result += ", "; + result += N.ToString(); + } + result += " ]"; + return result; + } + public override string ToString(string aPrefix) + { + string result = "[ "; + foreach (JSONNode N in m_List) + { + if (result.Length > 3) + result += ", "; + result += "\n" + aPrefix + " "; + result += N.ToString(aPrefix+" "); + } + result += "\n" + aPrefix + "]"; + return result; + } + public override void Serialize (System.IO.BinaryWriter aWriter) + { + aWriter.Write((byte)JSONBinaryTag.Array); + aWriter.Write(m_List.Count); + for(int i = 0; i < m_List.Count; i++) + { + m_List[i].Serialize(aWriter); + } + } + } // End of JSONArray + + public class JSONClass : JSONNode, IEnumerable + { + private Dictionary m_Dict = new Dictionary(); + public override JSONNode this[string aKey] + { + get + { + if (m_Dict.ContainsKey(aKey)) + return m_Dict[aKey]; + else + return new JSONLazyCreator(this, aKey); + } + set + { + if (m_Dict.ContainsKey(aKey)) + m_Dict[aKey] = value; + else + m_Dict.Add(aKey,value); + } + } + public override JSONNode this[int aIndex] + { + get + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return null; + return m_Dict.ElementAt(aIndex).Value; + } + set + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return; + string key = m_Dict.ElementAt(aIndex).Key; + m_Dict[key] = value; + } + } + public override int Count + { + get { return m_Dict.Count; } + } + + + public override void Add(string aKey, JSONNode aItem) + { + if (!string.IsNullOrEmpty(aKey)) + { + if (m_Dict.ContainsKey(aKey)) + m_Dict[aKey] = aItem; + else + m_Dict.Add(aKey, aItem); + } + else + m_Dict.Add(Guid.NewGuid().ToString(), aItem); + } + + public override JSONNode Remove(string aKey) + { + if (!m_Dict.ContainsKey(aKey)) + return null; + JSONNode tmp = m_Dict[aKey]; + m_Dict.Remove(aKey); + return tmp; + } + public override JSONNode Remove(int aIndex) + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return null; + var item = m_Dict.ElementAt(aIndex); + m_Dict.Remove(item.Key); + return item.Value; + } + public override JSONNode Remove(JSONNode aNode) + { + try + { + var item = m_Dict.Where(k => k.Value == aNode).First(); + m_Dict.Remove(item.Key); + return aNode; + } + catch + { + return null; + } + } + + public override IEnumerable Childs + { + get + { + foreach(KeyValuePair N in m_Dict) + yield return N.Value; + } + } + + public IEnumerator GetEnumerator() + { + foreach(KeyValuePair N in m_Dict) + yield return N; + } + public override string ToString() + { + string result = "{"; + foreach (KeyValuePair N in m_Dict) + { + if (result.Length > 2) + result += ", "; + result += "\"" + Escape(N.Key) + "\":" + N.Value.ToString(); + } + result += "}"; + return result; + } + public override string ToString(string aPrefix) + { + string result = "{ "; + foreach (KeyValuePair N in m_Dict) + { + if (result.Length > 3) + result += ", "; + result += "\n" + aPrefix + " "; + result += "\"" + Escape(N.Key) + "\" : " + N.Value.ToString(aPrefix+" "); + } + result += "\n" + aPrefix + "}"; + return result; + } + public override void Serialize (System.IO.BinaryWriter aWriter) + { + aWriter.Write((byte)JSONBinaryTag.Class); + aWriter.Write(m_Dict.Count); + foreach(string K in m_Dict.Keys) + { + aWriter.Write(K); + m_Dict[K].Serialize(aWriter); + } + } + } // End of JSONClass + + public class JSONData : JSONNode + { + private string m_Data; + public override string Value + { + get { return m_Data; } + set { m_Data = value; } + } + public JSONData(string aData) + { + m_Data = aData; + } + public JSONData(float aData) + { + AsFloat = aData; + } + public JSONData(double aData) + { + AsDouble = aData; + } + public JSONData(bool aData) + { + AsBool = aData; + } + public JSONData(int aData) + { + AsInt = aData; + } + + public override string ToString() + { + return "\"" + Escape(m_Data) + "\""; + } + public override string ToString(string aPrefix) + { + return "\"" + Escape(m_Data) + "\""; + } + public override void Serialize (System.IO.BinaryWriter aWriter) + { + var tmp = new JSONData(""); + + tmp.AsInt = AsInt; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.IntValue); + aWriter.Write(AsInt); + return; + } + tmp.AsFloat = AsFloat; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.FloatValue); + aWriter.Write(AsFloat); + return; + } + tmp.AsDouble = AsDouble; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.DoubleValue); + aWriter.Write(AsDouble); + return; + } + + tmp.AsBool = AsBool; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.BoolValue); + aWriter.Write(AsBool); + return; + } + aWriter.Write((byte)JSONBinaryTag.Value); + aWriter.Write(m_Data); + } + } // End of JSONData + + internal class JSONLazyCreator : JSONNode + { + private JSONNode m_Node = null; + private string m_Key = null; + + public JSONLazyCreator(JSONNode aNode) + { + m_Node = aNode; + m_Key = null; + } + public JSONLazyCreator(JSONNode aNode, string aKey) + { + m_Node = aNode; + m_Key = aKey; + } + + private void Set(JSONNode aVal) + { + if (m_Key == null) + { + m_Node.Add(aVal); + } + else + { + m_Node.Add(m_Key, aVal); + } + m_Node = null; // Be GC friendly. + } + + public override JSONNode this[int aIndex] + { + get + { + return new JSONLazyCreator(this); + } + set + { + var tmp = new JSONArray(); + tmp.Add(value); + Set(tmp); + } + } + + public override JSONNode this[string aKey] + { + get + { + return new JSONLazyCreator(this, aKey); + } + set + { + var tmp = new JSONClass(); + tmp.Add(aKey, value); + Set(tmp); + } + } + public override void Add (JSONNode aItem) + { + var tmp = new JSONArray(); + tmp.Add(aItem); + Set(tmp); + } + public override void Add (string aKey, JSONNode aItem) + { + var tmp = new JSONClass(); + tmp.Add(aKey, aItem); + Set(tmp); + } + public static bool operator ==(JSONLazyCreator a, object b) + { + if (b == null) + return true; + return System.Object.ReferenceEquals(a,b); + } + + public static bool operator !=(JSONLazyCreator a, object b) + { + return !(a == b); + } + public override bool Equals (object obj) + { + if (obj == null) + return true; + return System.Object.ReferenceEquals(this, obj); + } + public override int GetHashCode () + { + return base.GetHashCode(); + } + + public override string ToString() + { + return ""; + } + public override string ToString(string aPrefix) + { + return ""; + } + + public override int AsInt + { + get + { + JSONData tmp = new JSONData(0); + Set(tmp); + return 0; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + public override float AsFloat + { + get + { + JSONData tmp = new JSONData(0.0f); + Set(tmp); + return 0.0f; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + public override double AsDouble + { + get + { + JSONData tmp = new JSONData(0.0); + Set(tmp); + return 0.0; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + public override bool AsBool + { + get + { + JSONData tmp = new JSONData(false); + Set(tmp); + return false; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + public override JSONArray AsArray + { + get + { + JSONArray tmp = new JSONArray(); + Set(tmp); + return tmp; + } + } + public override JSONClass AsObject + { + get + { + JSONClass tmp = new JSONClass(); + Set(tmp); + return tmp; + } + } + } // End of JSONLazyCreator + + public static class JSON + { + public static JSONNode Parse(string aJSON) + { + return JSONNode.Parse(aJSON); + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/SimpleJSON.cs.meta b/Assets/Plugins/SimpleJSON.cs.meta new file mode 100644 index 0000000..8601ab7 --- /dev/null +++ b/Assets/Plugins/SimpleJSON.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: ef4cbfe52ee0546cc9d2d7bf44dda929 +timeCreated: 1480804441 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/PackageAssembler.cs b/Assets/SerialComm/Scripts/PackageAssembler.cs new file mode 100644 index 0000000..cd50eac --- /dev/null +++ b/Assets/SerialComm/Scripts/PackageAssembler.cs @@ -0,0 +1,68 @@ +using System.IO; + +// The package format used in this class is described in UnityThreads.cpp. +public class PackageAssembler +{ + public const int packageSize = 64; + + public void AddPackage(byte[] package) + { + if(package.Length != packageSize) + return; + + var input = new BinaryReader(new MemoryStream(package)); + + var magic1 = input.ReadUInt32(); + var messageId = input.ReadUInt32(); + var packageNumber = input.ReadUInt32(); + var nBytesOfData = input.ReadByte(); + var data = input.ReadBytes(47); + var magic2 = input.ReadUInt32(); + + if(magic1 != 0xaaaa5555 || magic2 != 0xaa55aa55 || + nBytesOfData > 47 || + (messageId == lastMessageId && lastMessageWasBad) || + (messageId == lastMessageId && packageNumber != lastPackageNumber - 1)) + { + lastMessageWasBad = true; + return; + } + + if(messageId != lastMessageId) + { + fullMessageReceived = false; + message = ""; + } + + for(int i = 0; i < nBytesOfData; i++) + message += (char)data[i]; + + if(packageNumber == 0) + fullMessageReceived = true; + + lastMessageId = messageId; + lastPackageNumber = packageNumber; + lastMessageWasBad = false; + } + + public bool FullMessageWasReceived() + { + return fullMessageReceived; + } + + public string GetMessage() + { + return message; + } + + public bool LastMessageWasBad() + { + return lastMessageWasBad; + } + + uint lastMessageId = uint.MaxValue; + bool lastMessageWasBad = true; + uint lastPackageNumber = 0; + string message; + bool fullMessageReceived = false; +} diff --git a/Assets/SerialComm/Scripts/PackageAssembler.cs.meta b/Assets/SerialComm/Scripts/PackageAssembler.cs.meta new file mode 100644 index 0000000..08daebf --- /dev/null +++ b/Assets/SerialComm/Scripts/PackageAssembler.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 3ce79d686201049cbb8889067d6f65d9 +timeCreated: 1480802177 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/RobotCommand.cs b/Assets/SerialComm/Scripts/RobotCommand.cs new file mode 100644 index 0000000..2e56965 --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotCommand.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +public class RobotCommand +{ + public RobotCommand(string commandDescription, string actualCommand) + { + this.actualCommand = actualCommand; + this.commandDescription = commandDescription; + + BuildActualParameterList(); + } + + public bool Is(string commandDescription) + { + return this.commandDescription == commandDescription; + } + + public int GetInteger(int i) + { + return Convert.ToInt32(actualParameters[i]); + } + + public string GetWord(int i) + { + return actualParameters[i]; + } + + public string GetString(int i) + { + return actualParameters[i]; + } + + void BuildActualParameterList() + { + Tokenizer commandDescriptionTokenizer = new Tokenizer(commandDescription, ' '); + Tokenizer actualCommandTokenizer = new Tokenizer(actualCommand, ' '); + + while(actualCommandTokenizer.HasMore()) + { + var formalParameter = commandDescriptionTokenizer.GetToken(); + + if(formalParameter == "#i" || formalParameter == "#w") + actualParameters.Add(actualCommandTokenizer.GetToken()); + + else if(formalParameter == "#s") + actualParameters.Add(actualCommandTokenizer.GetString()); + + else + actualCommandTokenizer.GetToken(); // The token is a keyword and we ignore it. + } + + } + + string actualCommand; + string commandDescription; + IList actualParameters = new List(); +} diff --git a/Assets/SerialComm/Scripts/RobotCommand.cs.meta b/Assets/SerialComm/Scripts/RobotCommand.cs.meta new file mode 100644 index 0000000..78b2f0a --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotCommand.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 1124f602d7eaf42138213effd05dc430 +timeCreated: 1480805087 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/RobotConnection.cs b/Assets/SerialComm/Scripts/RobotConnection.cs new file mode 100644 index 0000000..f74c72a --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotConnection.cs @@ -0,0 +1,249 @@ +using System; +using System.Text; +using System.Collections.Generic; +using System.Threading; +using System.Net.Sockets; +using System.IO; +using UnityEngine; + +public interface RobotMessageReceiver +{ + void NewMessage(string message); +} + +public interface RobotMessageSender +{ + void SendMessage(string message); +} + +// The package format used in this class is described in UnityThreads.cpp. +public class RobotConnection : MonoBehaviour, RobotMessageSender +{ + public RobotConnection(string server, int port, RobotMessageReceiver robotMessageReceiver) + { + this.server = server; + this.port = port; + this.robotMessageReceiver = robotMessageReceiver; + + inputThread = new Thread(new ThreadStart(InputThread)); + outputThread = new Thread(new ThreadStart(OutputThread)); + + inputThread.Start(); + outputThread.Start(); + } + + public void SetServer(string server, int port) + { + lock(serverPortLock) + { + this.server = server; + this.port = port; + } + } + + public void Stop() + { + stopped = true; + inputThread.Join(); + outputThread.Join(); + + CloseSocket(); + } + + public void SendMessage(string message) + { + lock(sendMessagesLock) + sendMessages.Enqueue(message); + } + + void InputThread() + { + InputOutput(InputFunction, ref inputLock, ref outputLock, ref inputHasConnected, ref outputHasConnected); + } + + void OutputThread() + { + InputOutput(OutputFunction, ref outputLock, ref inputLock, ref outputHasConnected, ref inputHasConnected); + } + + void InputOutput(RobotConnectionFunction function, ref object myLock, ref object otherLock, ref bool hasConnected, ref bool otherHasConnected) + { + while(!stopped) + { + lock(connectingLock) + { + otherHasConnected = false; + } + + try + { + lock(myLock) + { + if(socket == null) + throw new Exception(); + + function(); + } + } + catch(Exception) + { + ResetConnection(ref outputLock, ref hasConnected, ref otherHasConnected); + } + } + } + + void ResetConnection(ref object otherLock, ref bool hasConnected, ref bool otherHasConnected) + { + lock(connectingLock) + { + if(otherHasConnected) + return; + + lock(otherLock) + { + if(stopped) + return; + + socket = null; + + while(!stopped && socket == null) + { + try + { + socket = new TcpClient(); + + IAsyncResult asyncResult; + lock(serverPortLock) + { + asyncResult = socket.BeginConnect(server, port, null, null); + } + asyncResult.AsyncWaitHandle.WaitOne(1000); + + if(socket.Connected) + socket.EndConnect(asyncResult); + else + throw new Exception(); + + socket.ReceiveTimeout = 1000; + socket.SendTimeout = 1000; + socket.NoDelay = true; + + hasConnected = true; + } + catch(Exception) + { + CloseSocket(); + } + } + } + } + } + + void InputFunction() + { + Debug.Log ("We have input"); + byte[] package = new byte[PackageAssembler.packageSize]; + NetworkStream stream = socket.GetStream (); + //enter to an infinite cycle to be able to handle every change in stream + while (true) { + if(stopped) + return; + + if(!stream.DataAvailable) + { + Thread.Sleep(1); + continue; + } + + Byte[] bytes = new Byte[socket.Available]; + + stream.Read(bytes, 0, bytes.Length); + + //translate bytes of request to string + String data = Encoding.UTF8.GetString(bytes); + Debug.Log (data); + robotMessageReceiver.NewMessage (data); + } + } + + void OutputFunction() + { + string message; + + while(!stopped && sendMessages.Count != 0) + { + lock(sendMessagesLock) + message = sendMessages.Dequeue(); + + SendAsPackages(message); + } + + var secondsSinceLastHello = DateTime.Now.Subtract(lastHelloTime).TotalSeconds; + if(secondsSinceLastHello > 5) + { + SendMessage("hello"); + lastHelloTime = DateTime.Now; + } + + Thread.Sleep(1); + } + + void SendAsPackages(string message) + { + var nBytesPerPackage = 47; + var nPackages = (message.Length + nBytesPerPackage - 1) / nBytesPerPackage; + var packageNumber = nPackages - 1; + var nBytesLeftInMessage = message.Length; + byte[] dataZeros = new byte[nBytesPerPackage]; // Elements are initialized to 0. + + messageId++; + + while(nBytesLeftInMessage != 0) + { + uint magic1 = 0xaaaa5555; + uint magic2 = 0xaa55aa55; + byte nBytesData = (byte)(nBytesLeftInMessage >= nBytesPerPackage ? nBytesPerPackage : nBytesLeftInMessage); + var data = message.Substring(message.Length - nBytesLeftInMessage); + + socket.GetStream().Write(BitConverter.GetBytes(magic1), 0, 4); + socket.GetStream().Write(BitConverter.GetBytes(messageId), 0, 4); + socket.GetStream().Write(BitConverter.GetBytes(packageNumber), 0, 4); + socket.GetStream().Write(BitConverter.GetBytes(nBytesData), 0, 1); + socket.GetStream().Write(Encoding.UTF8.GetBytes(data), 0, nBytesData); + socket.GetStream().Write(dataZeros, 0, nBytesPerPackage - nBytesData); + socket.GetStream().Write(BitConverter.GetBytes(magic2), 0, 4); + + packageNumber--; + nBytesLeftInMessage -= nBytesData; + } + } + + void CloseSocket() + { + if(socket != null) + socket.Close(); + + socket = null; + } + + delegate void RobotConnectionFunction(); + + string server; + int port; + RobotMessageReceiver robotMessageReceiver; + volatile bool stopped = false; + TcpClient socket; + Thread inputThread; + Thread outputThread; + bool inputHasConnected = false; + bool outputHasConnected = false; + object connectingLock = new object(); + object inputLock = new object(); + object outputLock = new object(); + object serverPortLock = new object(); + PackageAssembler packageAssembler = new PackageAssembler(); + object sendMessagesLock = new object(); + Queue sendMessages = new Queue(); + int messageId = 0; + DateTime lastHelloTime = DateTime.Now; +} diff --git a/Assets/SerialComm/Scripts/RobotConnection.cs.meta b/Assets/SerialComm/Scripts/RobotConnection.cs.meta new file mode 100644 index 0000000..b9c8242 --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotConnection.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 82ae2e0e5268a47d7a34185f5858d114 +timeCreated: 1480802177 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/RobotMessages.cs b/Assets/SerialComm/Scripts/RobotMessages.cs new file mode 100644 index 0000000..39f4d12 --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotMessages.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +//using SimpleJSON; +using UnityEngine; + + +//Basic structure for elements containing a chat message +public struct RobotChatMessage { + + public string user; //user will eventually need user type + public string message; //message is the text input from the user, unless it's a command. + //TODO: messages should all be messages, except we assign a message type to them in the future. + public bool isCommand; //Is this message a command? + public bool isExecuting; //Is it executing? +} + +//Message parameters are defined and set here to determine how they are displayed on the GUI +public struct InternalRobotMessage { + + + //Set these values for a normal chat message. + public InternalRobotMessage(string user, string message) + { + this.user = user; + this.message = message; + commandDescription = ""; + commandId = 0; + isCommand = false; + isExecuting = false; + newMessage = false; + } + + //Set these values if the message is a command + public InternalRobotMessage(string user, string message, string commandDescription, int commandId) + { + this.user = user; + this.message = message; + this.commandDescription = commandDescription; + this.commandId = commandId; + isCommand = true; + isExecuting = false; + newMessage = false; + } + + //These values are set by InternalRobotMessage method above + public string user; + public string message; + public string commandDescription; + public int commandId; + public bool isCommand; + public bool isExecuting; + public bool newMessage; +}; + +//Gets messages from another source over the network via TCP Sockets +//Class uses RobotMessageReceiver & RobotMEssageSender interfaces from RobotConnection.cs +public class RobotMessages : MonoBehaviour, RobotMessageReceiver, RobotMessageSender +{ + + IList chatMessages = new List(); + IDictionary variables = new Dictionary(); + RobotConnection connection; + public RobotMessages(string server, int port) + { + connection = new RobotConnection(server, port, this); + } + + public void SetServer(string server, int port) + { + connection.SetServer(server, port); + } + + public void Stop() + { + connection.Stop(); + } + + //Message package is assembled from string + public void NewMessage(string message) + { + Debug.Log ("You hit here"); + Debug.Log(message); + AddMessage(new InternalRobotMessage ("", message)); + Debug.Log ("last"); + } + + int maxMessageNumber = 100; + public void SetMaximumNumberOfMessages(int maxMessageNumber) + { + this.maxMessageNumber = maxMessageNumber; + + TrimChatMessages(); + } + + //Package up the components of the message into a List + //TODO: This should not be directly referencing Constants, this class should be as encapsulated as possible. + + object chatMessagesLock = new object(); + public IList GetChatMessages() + { + var copyOfChatMessages = new List(); + lock(chatMessagesLock) + { + Debug.Log ("Made it to chat messages"); + Debug.Log (chatMessages.Count); + foreach(var message in chatMessages) + { + var chatMessage = new RobotChatMessage(); + chatMessage.user = message.user; + chatMessage.message = message.message; + chatMessage.isCommand = message.isCommand; + chatMessage.isExecuting = message.isExecuting; + + copyOfChatMessages.Add(chatMessage); + } + } + + return copyOfChatMessages; + } + + //Get specific variables from broadcast source related to the robot. + object variablesLock = new object(); + public IDictionary GetVariables() + { + lock(variablesLock) + { + return new Dictionary(variables); + } + } + + IList commands = new List(); + object commandsLock = new object(); + + public IList GetCommands() + { + lock(commandsLock) + { + IList returnCommands = commands; + commands = new List(); + return returnCommands; + } + } + + public void SendMessage(string message) + { + connection.SendMessage(message); + } + + void AddMessage(InternalRobotMessage message) + { + lock(chatMessagesLock) + { + chatMessages.Add(message); + } + + TrimChatMessages(); + } + + void AddCommand(InternalRobotMessage internalRobotMessage) + { + lock(commandsLock) + { + var command = new RobotCommand(internalRobotMessage.commandDescription, internalRobotMessage.message); + commands.Add(command); + } + } + + void SetCommandIsExecuting(int commandId, bool isExecuting) + { + lock(commandsLock) + { + for(int i = 0; i < chatMessages.Count; i++) + { + var message = chatMessages[i]; + + if(message.commandId == commandId) + message.isExecuting = isExecuting; + else + message.isExecuting = false; + + chatMessages[i] = message; + } + } + } + + void SetVariable(string variable, string value) + { + lock(variablesLock) + { + variables[variable] = value; + } + } + + void TrimChatMessages() + { + lock(chatMessagesLock) + { + while(chatMessages.Count > maxMessageNumber) + chatMessages.RemoveAt(0); + } + } + + + // 20160603 rtharp + // moved out to RobotStuff, so we can have multiple connecctions + // but only one set of chat & variable + // since we only have the one display of them + //IList chatMessages = new List(); + //IDictionary variables = new Dictionary(); +} diff --git a/Assets/SerialComm/Scripts/RobotMessages.cs.meta b/Assets/SerialComm/Scripts/RobotMessages.cs.meta new file mode 100644 index 0000000..6c24cf0 --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotMessages.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 858a879b66ce842ba83d5010739168c9 +timeCreated: 1480804241 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/TellyNetConnect.cs b/Assets/SerialComm/Scripts/TellyNetConnect.cs index dabce10..40453a9 100644 --- a/Assets/SerialComm/Scripts/TellyNetConnect.cs +++ b/Assets/SerialComm/Scripts/TellyNetConnect.cs @@ -2,58 +2,58 @@ using System.Collections; using System; using System.Threading; +using System.Collections.Generic; -public class TellyNetConnect : MonoBehaviour { +public class TellyNetWrapper : MonoBehaviour { - int messageId = 0; + public RobotMessages robotMessages; - // Connection to Tellynet Server - string tellynetServer = "ec2-54-191-54-225.us-west-2.compute.amazonaws.com"; - string tellynetPort = ":3000"; - string tellynetSocketProtocol = "ws://"; - - // Serial connection to Robot - public string portName = "/dev/cu.usbmodem12341"; - public int baudRate = 9600; - public int delayBeforeReconnecting = 1000; - public int maxUnreadMessages = 5; + public TellyNetWrapper(string server, int port) { + robotMessages = new RobotMessages (server, port); + } +} +public class TellyNetConnect : MonoBehaviour { + TellyNetWrapper tw; + SerialThread serialThread; + Thread thread; + // Use this for initialization IEnumerator Start () { + // Connection to Tellynet Server + //string tellynetServer = "ec2-54-191-54-225.us-west-2.compute.amazonaws.com"; + string tellynetServer = "127.0.0.1"; + int tellynetPort = 3000; + // Serial connection to Robot + //string portName = "/dev/cu.usbmodem12341"; + string portName = "/dev/tty.ArcBotics-DevB"; + int baudRate = 9600; + int delayBeforeReconnecting = 1000; + int maxUnreadMessages = 5; + tw = new TellyNetWrapper ("127.0.0.1", 8000); // Connect to the robot and start the serial thread - var serialThread = new SerialThread(portName, baudRate, delayBeforeReconnecting, maxUnreadMessages); - var thread = new Thread(new ThreadStart(serialThread.RunForever)); + serialThread = new SerialThread(portName, baudRate, delayBeforeReconnecting, maxUnreadMessages); + thread = new Thread(new ThreadStart(serialThread.RunForever)); thread.Start(); - // Connect to the Tellynet web socket server - WebSocket w = new WebSocket(new Uri(tellynetSocketProtocol + tellynetServer + tellynetPort)); - yield return StartCoroutine(w.Connect()); - - // Start Message Loop - while (true) - { - string reply = w.RecvString(); - string botReply = serialThread.ReadSerialMessage (); - - if (reply != null) { - serialThread.SendSerialMessage (reply); - Debug.Log (reply); - } - if (w.error != null) { - Debug.LogError ("Error: "+w.error); - break; - } - - if (botReply != null) { - Debug.Log("Bot replied: " + botReply); - } - - yield return 0; + yield return 0; + } + + void Update() { + var messages = tw.robotMessages.GetChatMessages(); + string botReply = serialThread.ReadSerialMessage (); + if (messages.Count > 0) { + string message = messages [0].message; + Debug.Log (message); + serialThread.SendSerialMessage (message); + } + + if (botReply != null) { + Debug.Log("Bot replied: " + botReply); } - w.Close(); } } diff --git a/Assets/SerialComm/Scripts/Tokenizer.cs b/Assets/SerialComm/Scripts/Tokenizer.cs new file mode 100644 index 0000000..cc5f8b1 --- /dev/null +++ b/Assets/SerialComm/Scripts/Tokenizer.cs @@ -0,0 +1,64 @@ +using System; + +class Tokenizer +{ + public Tokenizer(string str, char delimiter) + { + this.str = str; + this.delimiter = delimiter; + } + + public string GetToken() + { + string token = ""; + + if(!HasMore()) + throw new Exception(); + + while(position < str.Length && str[position] != delimiter) + { + token += str[position]; + position++; + } + + return token; + } + + public string GetString() + { + if(!HasMore()) + throw new Exception(); + + var stringBegin = position; + position = str.Length; + + return str.Substring(stringBegin); + } + + public string GetString(int nChars) + { + if(!HasMore()) + throw new Exception(); + + string returnString = str.Substring(position, nChars); + position += nChars; + return returnString; + } + + public bool HasMore() + { + SkipDelimiters(); + + return position < str.Length; + } + + void SkipDelimiters() + { + while(position < str.Length && str[position] == delimiter) + position++; + } + + string str; + char delimiter; + int position; +} diff --git a/Assets/SerialComm/Scripts/Tokenizer.cs.meta b/Assets/SerialComm/Scripts/Tokenizer.cs.meta new file mode 100644 index 0000000..03b8666 --- /dev/null +++ b/Assets/SerialComm/Scripts/Tokenizer.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9ac90bd4871624965a3b36eacb34c07c +timeCreated: 1480804241 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 1d8dc69ca3f2218e259f6f1b2db3366d39d00834..c186628af57b4cae50ec5c5cc7ec421e6378772c 100644 GIT binary patch literal 44357 zcmdU&d7Kk7lIf(UyXhqh3O0zy zCdwi%>sh;wpM|1>b=! zatpS{YXV+OU3f?P*Wx|xClVq>d_TM==Zh)gZ^C=B%?ObqJ_GN`Hz!1*KMRAP{bu(* z2(}>1dHxgdV{u9Qs64bMFACoZKbaz(lim9u*qSgmu5%VHsk04U5kDH`x&oK1pKZz8 zw4cy54e$6O-wwDdM?TTz`&v9homY{&*Ti56Y}ew&Yu2CVT>&dn!djVFDBMA>*qear=L4J=J_G;=bN}@UGAd1D|iz8G2y%7 zXAn$wI@dtw=eVNIZp1PE46ev`CwAPo%X7FQ--8$_YL|bw_d&pZ6xBcd->J*{|2lG{ zr2l(5*6IIV%6-05p~HOl#)~QHC%wWaJ37u+2;V2c$As^j;AaZoFTpPnJ|)3#5dQiY zXFtCi9RCOVC-_6cF?6+l9z(l41CapqFgPp%L?J;Co2&iKI;**_zErpL-R zpXl?QmEgMxpPk?s8_rL}ORazUzfkz$3Hy_T&q?r8gm)+SxxyLem?HnL6#f>Em48n7 z5efcH;YTJo#<27Ms09C&@LYobOL$L$?|{z0_L`gE`v~t%@NVIK2|iyqXE3IyyhFn0 zd#t=9d_jVrBK+tCKTmjnf`3x@F$sQ`aLy-8kv|U$M_X&VGk*R89RCLc!B+^cCiwBfYveiaC)Y& z?#hwdd96Y2UUps7RPNVZP0??Gr|GY!eXdh~1G#%y{f)}I0?x~4i2f#UCe)kp{}hiq z{C|6lZ^HVA^D_JYJCsi{>$g+Ey+4yo|36K+Uq4?Y_54oN@%&cdZSowoJbzsH>EuWe ze^K}uggMVS-nYP_aT3tG$dRIPF5}9%0UNmg_9BKc5^aD%WMg-|MmC^#2ZU`hNj=oA%cK z_W`H*|9*1!vi@JF+_%d&#LtVsnIiw67JjkE%Ks|-k_6v!E1%y75_}KgmnQfD!aqo! zvbk zTb<7+_c~{Z&Sx`pZdQA%^Eu^S=Q`23B}3;{wYNIADfc=Lh|cX9I-gg2t8<5Puk#bp zxidrOF15EhUr_FKUKX7%X6W3l_EzT}KaVK4{(LXSx5rh-_lIl1S^qy!ZsXez!M(oi4?j}wb?y|MCo*)NRC}xQW8s{SmZL8` zB7R~{aVeUQUKIY6*fTy~1N)bR|J0jk+@&@Co~v0=;xma+vJnndF+1MdH-KhzM1j=FW}z)&0YOR=e>U=cdyOO zdGFtpPjdT$>7xI4aG$T$|A%s`|4(wHE}V>`PF&oh|NkOvlW$@Cd|COH#?M#Ck-Bio zySKzo>Nw}yZ7$T2_Ax)v@|pD);^52&w0-RNs!%t(Dt;vJE*B`w9DV)K9im`$?uAwgdNmZf5G? zRpd@*GgA+*R_@39vG8*e?6#*d6307YS9_K49b%mEd?PrzZ?L0s8*h1D&RdLo@i_1r z<$nKrzSPgoG;TY4Q?6ZTpCh;Rv#aXZak`swuYa}Z@1CK*M~41uRmbYTPPyz`p-<^O z`3Dl~Z6QkXknp_{{D;E#CeL|$?$duGeoj_<+fVjUZtG#+7=IZW>?f~*^nS|yJiG0y zeEUvG@EwJ}KE~O;yMwcR_gC)w)qP;>CvO1ftuCC_#M}3c%62tM8Jz8MBzc?qwq1@=ZrdfN zIunDhLL+LI9<{gaGFQ2+H-4U$)-HYIuDrHg<|&`##_tozn|Aa02NLIHj_VhMFCfp6 z`|`dlWEEuiTdR7}cK`?Dp#M@*b=9w!90K+wu;8r#rC6QilH84E=SgW81f^+|Ms(K%f1MpNF%8qIJs$gjZsm>!|C+pDKB~ zv%EFx=X|-i{)yJrCzCsU>rY*|FYjHV-^kE!X6Uz6$LgYt|Ex7YK~pMEqyG8e-XFUzdarW7zB*NOF38Y%pW0iU_bd1P;Zo>uUbv7t zDfNF5?Q`_g*8jz-WBbD;%6+}vAocJ8>LanVHzXZ{H#4PeRpPPkp3lToi=g5aaCs zr-HNpe@eOC7kpavZNI%yx$i$0K!*}H@sHeizE?@UpCLzz`rF-N|JelpvGAK?I_y_} z2j{r>9C@2MwqM->p4RTSlDm9uz1^nVx65`rdi~pJoTC4E+UKZc$K4&OWA*P;?)CQ) z{kt;szmTE-Mb)wTcPsb$hl>6^8Twz!(7#u8tp1mkd;K2K|4N4beHr@qtB%!wK)LU? zi=fYb`ylm^*iYE*CrG|uC2y14cK;f6a-Hq+b=9}|enYv}9~1qDGW5Thq5mz_vHA}y z_xk6G{>ct z^L<$I{Q>onqH+BcIMEOJN6vG`;TOdIM+y6hJ9+yj5`1srPbT;@;Xh9B`NDrf9?SPc zaQ3UGV)pD;W8jP@KP7L|2ivcH2A)f}1}{k`hjd|y=V>+@Tp|A!3yKW6CvNp-CLpOyP^Jt5_KiTX%Uxt^7L z|3cpGEZ1MDlj|(k-&Eh``*-C&-CY?VcqG|xg9#D$_55bh?*dQLpFsN*{fVk$%R5QA*B=u7%`)^i&(PmOb*%oD z%Dw(t(cdaVf9nkWZB)nVZ>!wvpCuL;uyPWA(RJ?)5Jg{T(v&cg)b|_p;O4 z_chAB{zpZB=M4Q_GW2&<9qa#Y%Dw)LqQ85F{vH|nuT>qZ|2pMf{~pobGedu`41Js_ zrPj}6-iyY*7H2_Hg#-2oDZH>&kM-$NOJxRA0j<!F_+Q=Y%JaI~{vYc%pLOUN?!KtEhi3pRs-o?M^?}&^||Q{Tx*t>t{*1?>|pKpY^qt`cB8k?H9p0 ze%FyBMe{}1ZoYl__q5Xd83RxAr$X*@tUp!dzPzsy{aS|p$r<`})v@{wTzBf2VSEO zGxSf-&_6?Utp2-{d;O~DpP8Zm?hO62RLAPSN4eKOP4v&s&_5?b|6J9v`sXS4`WK7- z`5F4}&CtI?(Eq6FSpAPF z_xihw{7QQL-aqJp?`CR{^wN3>ffT=>t7=Jw`SRA2HEBE@J5dAwc^zY2jze{zj{uh*c{o6$Uiy8WNXXxLfI#&Nn%Dw)BqJM9O{+BcK zzoI%;|32kj|4GrmKSTe44E+aH$LfDox$plkL7)BqYt%=I`u~=Dc>eVmXMf%Uoc;M5 z{1EkX(Di{rQ%1U)}>m|KSY%Z)fO#M|G_Jca{6{9wy~_g!;}OpKqV! z`#th@XSp7wPOh_DkEy=R_xsAd{&LZOJVXBn8Tvm|9jpH%<-S~@l!F_+T`-SI}`*xWP9rov6Qm4&lY<&I|?OndMKR>TJwq1U$+<%X& zU-Vya`t1(=-zc~Gza@u597lVjywU#dcWUpyAA1>$Cc*OeYHz=P^`hF_?_d1^yemh2 z`~9mwlDmBE_pkn>+}GPlQm#KceLiFB?IovAZp-x-r{nn&_#S<)@2|?e{>h^Mw+#Ki zXXyV!b!@x*Q@PiFx9I;XL;vLr{Z~}S>Z6z_D_>s!Bck60j_L%p^)rF?IdWS+6II9R zPg3snZxQ{?z|-{I@8?L--$He){+7zU{zIa_Rfayk-a-H?IJvkm>ea8FM4$A#_-1T*SyzdB}roWSN+b*vm zM@o*zoz>os$6eIkj>lcq-i}B2dsA}Mx8rel*!yzX@wkU_-(LGjx$wQe)N;L!_D0m(cedPtp2{r{roim{YjAC51fhpf%DWVaF%~cf^QK1 z`UJl~`2OTMufub*YeoMJF?)BuD*TNxz8v=71;^h$ z;crUt-wHn{!GACOVCDYr-~9oc`Qkh6NaOlZzK5#4_b1ACYJx}kPD}78-|5PIJ@2$9 z%M%1Mz@vJ55#>D$ob%&M@_T7xE~-^^Z_)^^YV+V!rYD_b9dB!u&n2oboNr_s@I4yK>ao z%AIFM-^-m#?)0}Z@v2w3*N^<~1NZ)0{du%c(Vwq6R)2wVKdu)_`(kY``nG-hY47xH z`yQh@c3dB;+@EhBC;I$7Pu_p4KcL*|FCs@`|5*o(X#KNT?S21whv*L~x9z(Gyemf? z+rCT5oxbfqL(0AW*`hxTp4NYs(LP0gx$0Q`70P}6Unu%3!PDyJINCdXTR(4A9b5lz zQ|{yH$Dlt6(#L~Ge>9uxgMc$)qQ z?Njs%s$=zw%Dw(Gq91~%>93}JivAkavHGLReLVaZ^jXdlI8!tq?XZ{UYsqt-a~?Se zob|je#yNi-DfZ=r&Jy8cF?+Vl8gRzj3VECQwq2^=T`phSE;VvIl5CfgmHYm1s`yz4 zXJWaoM!x5XKaB*xQg}1LuNU4*@XrfhPoDGoEY}0zEY}9|Hn}a=M({3YZ_Blb9FHW+ z75yH#Xx;d2(SLh}{yQ@CPtDLjO}X!HPl^6JGxXaT`lqXo?Qdr&_xdl2{<|{t&&<$& zx9V8^vy^-N348nce@}+~*%|uhsE*Y?SGm{UP4v&p&_6#z|GlbX^)FEF^$!sJ_hsn6 zKSTdQ)v@{)DfjwEiT=eI`j=$re?WDt{-w(OxLX8$#)l75A1Ug$dCB)f;J>0d$n6#Xkz$Le3D-0Qzf^sfd_)4zuHDf%B( z9jpH_u7Uf?5N1}f#c$)rgv`^8$U3IMf z=aqZ?zli=F;A#4I(mqB1F4eL6Ur_GH-7b^;xceeF6UQCL(V>$)zdOb`Pb~)LJarFw zoA!1*eu?@yayw7mOO8j9^VFA>`*p?0WN-Hs@MwPII^h)I_r*B(UzdW@pZm%0<+IkG z2gut#-*|j@kQ|Br#P=^>ReQVt`kHdPe*QXmSB^S%{rnAbr*GG{4=JA%TsIlNCmMB1 z|Gx?D{oKs$oACE!9RHT`&CR~)Vd}Vi{omJ);@-E(UA|kG_2+k#d;PnSH}m~2xX;(> zKSKKy{qL!c)qhmE|NhMV(4Pe9$H1BR`vma5{q4y<-|s88`97}Pmg@%z&ixMcf2iD- z>j$F$Bl0%$wfawhcjd^f{*&Z*ggm}(|FLpkuIDEEa{UCHDJs`LCi`+dMV=$K`TkVx zZN5KC@Tgo*EBD|3*?J#;p8s>zx99o40Qdge^ZaL&`~Bu#&|$kjOPw~KvE$`A<#zo3 zk{pTSg>fL-Z~jW{?f0pk2k*+!-hQ9z*W^yeexK?E<^K1>9|k|!zP|xyVqDq*`5q_! z|CYQ>ZvFqAa_j%^Rmb}OqS{;k{{Y^VqrUb3kL1oD>;IpW`}nz5{QomJQxsRpwmoCo1>z#S!q6c9X!FSkIg-C!goyYg7BRZyea%O34XTlofG_W;kzXG4Z?R#@VkWXMxOKS%l`Qg zIQ!@BKle~>`{!%Pk&=bqqQIojL)xfi+9vHf#z<$nG4l=wMW^{t=# zsJ``cU)8bexBZlR{T-+HdYA$pwCR)8e?9G;|5ks0a_7I*e}i(bf5a58|3>g2MgIWW zr|2I@o}&LI zz5bI^y#7q^AVq%`?Njt;lc(q(rrhgKe7)B{96U(TpF{f;{chEe+zh! zqJISKQ}mBi9jkwoa<88k{Tz6ZqTfUN6#coXWA%HLd;Lp9zYjb}(Vs{A6#eMm2T1Nm z79{j<5dEVw^!qdPk5L`#|FO!w|96T0Lh9cOUlZeZAVYtV>RA27%Kduj8POl4e!EkD ziE(al8pDVzFcBh{!m0LfLBS%X5 z`Bt^Je!dMn&Clb>osRYM1m)h(o%i?S@kG_P@phGR>*q=2PT#JJ@@jAW905=Bvq0{2 zte-{Y-p@nD&rtQPpR1KyKi7~uee36_+FL)}znhVxPqy9H!amK5Z-053CPgQ&C=V{<+e!i33=~zG8%DtcO7e7x| zee35L%B`R8B6s@M&okBD`uT3~G(XQGcRJS3_bB&%UN3&0t@_r_bCg>@&n0*I*3a|Q z-uihyc$%N@C3ia3&kK}$Kffw|zEAb7pYK<0{k)Le>03W9QhV#?#o%dvUPA74te+oH z?)`jL{Jd24t)CxMZvFfaxzo3Pepv0TpC18F^Yb!tr(^xRT)Fo%c!Q7gSE#=A^GfB` z&#TCtzV-8JwYPp=1D@vRN6DRz_48xOy`OuCpVz9s_47LA*3XZVJALctC)D2h`AP6J zKd&cuI@Zq{lzTr97e7Cx`qs}+E4O~$NbdBlpEs$!_46~}X?}i|-04_9Z&vR8Tq1sc zPW7#yw*tr0dq2++Kkrq2>*tr1TR*=-?)0sn_o==0^M3F&KOZ1>I@Zqz zm3u$05kJ35{d@V0_48}wZF1}9*U6ER>yU4#z4h}U@H9WaN$zy4pWjk0`vvjyVb!;O zep~gepWjg(yI=UOao} zV!c`_4%VxyOXaX06vNf|R=GJ;sE1)?alI5)n)zm_S_zu@`kJuW7p^ZA!l2a%7gzei zv3#YtG;CB`^+MPP@`XYOiRxI`4D0jj;mNJAQrHv}s$;c$Wz(YkSQvTc-b+rst$L$c z55`u_NB(elY}H`CIf^$0cjM@4vr(%ygN?x^+#0Ht^NmsO+)#55Kdw%kXDr8T2X%beMYaKut?Nj$b{X;>+Sb+iBrfa`(k z2#S=XJ%wgzeb`$qSL;KyJn9mL!)m0DSvJ(n7uF5eVLrU6hN>=A8!WcdLgD%=#Zn>P ztkxG-7FP2`xDcRa*slM<3OYTtT7P9O}kP=DmqL!Tpg^|oArFD`S0#6t2Ii+kSb|b z3sHtfAzurZy4Fa^LoF7DtDCM<4yg<5v*@NXrq3-k`+QmEwpOoZ7cVvPBjvC!tTji` z{fC<9rlqpK*@&9AzcIg6F8iJ~&K6-TtVfeZbrRHDm3Zw1aJSj2heK$BawtzPULV%$ zDCgwx<>+yb6VT)+4ag zW8m0QB`jc^;yG{O0o#Ke7n!Vy-JD-)R>DRjSdWrG&D5c5Xx4ti1)-r0rDg%+DPI_8 z1Jz;}6lWCYbfRg~5{w=V)xxka+BY}m!O>y97>{q`jxT71jd(#&4MX{{TA3xN%qTX{ zn^*(rH}$aEC{$~u!tzqM!QJyk9~wp578jNp&Be85j4vPBR4D|l(y~T4Z8rNQyP)*S z(wYjIrl(vR&G(i=soKSrcxR)FAOqD(6`f%X{8<>T56iAl;D*&2GfX^Oj}BNJ2*;}R zP3UjS!+JB^7FICsH7u%T31HoQF6Q6La%r_Sm`Ae7$IH3{=@c2g|L7 z!I-ELN-i4tp%!&hW-xBmi@4SddKxwK&!z144MbVVA2BP=XLipBW_8aDrp@l2HkE(Q z2^Kd;!+IkaL2N`nEn+?_tuCPu9-tyQ6fv=d41|_1tLqzBWEfV1{p^Gz(hV`)$`VO3Ne5kJ%y(C6^>mlZv6_BlAny3dogU8O>h{XmU;c|jYA=fS#keHeq zfpczI1>vyB=*dFC;~hSOew8?tO%N| z#@u}U=u#0~3uf~+mYRzzLoM{@N)gv;;TKBcd~5+FvHY4)Y)dp{9Q19)1zV6{Q8AKSolM>sU!S>!V^Ci+l26*8! zm>amrse>X~phvqnyF6bnDEd-Ue=YGyb`v z)Esqf>LKp=P1e{d*;$!K=jO4#w%jqCXd~3wSu#QYI9N8Tvow+i)B@D-o^r!!kp+sx4ZBwfP6W6JiyL# z@yOcVWtBp{g{`^TH{^ldI?e@_hBYig(X=r4i%hgSOsTReThPx9F4Y56j-*!0AQ!JKPzi`Js=Lx%0AA*2u;FO9ijy<(682DGsgEA*;w#K%SQlg)a{j< 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 C-ufi~ diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 8c353d8..96b25e7 100755 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 5.1.2f1 +m_EditorVersion: 5.4.1f1 m_StandardAssetsVersion: 0