Browse Source

[unity] Providing error dialog and verbose error log message in case of version mismatch. Also providing error description when viewing skeletonData in inspector. Closes #1463.

Harald Csaszar 6 years ago
parent
commit
1a34ed59d5

+ 25 - 9
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SkeletonDataAssetInspector.cs

@@ -40,7 +40,7 @@ using System.Collections.Generic;
 using UnityEditor;
 using UnityEngine;
 
-using Spine;
+using CompatibilityProblemInfo = Spine.Unity.SkeletonDataCompatibility.CompatibilityProblemInfo;
 
 namespace Spine.Unity.Editor {
 	using Event = UnityEngine.Event;
@@ -69,6 +69,7 @@ namespace Spine.Unity.Editor {
 		SkeletonData targetSkeletonData;
 
 		readonly List<string> warnings = new List<string>();
+		CompatibilityProblemInfo compatibilityProblemInfo = null;
 		readonly SkeletonInspectorPreview preview = new SkeletonInspectorPreview();
 
 		GUIStyle activePlayButtonStyle, idlePlayButtonStyle;
@@ -140,9 +141,9 @@ namespace Spine.Unity.Editor {
 				return;
 			}
 
-			targetSkeletonData = warnings.Count == 0 ? targetSkeletonDataAsset.GetSkeletonData(false) : null;
+			targetSkeletonData = NoProblems() ? targetSkeletonDataAsset.GetSkeletonData(false) : null;
 
-			if (targetSkeletonData != null && warnings.Count <= 0) {
+			if (targetSkeletonData != null && NoProblems()) {
 				preview.Initialize(this.Repaint, targetSkeletonDataAsset, this.LastSkinName);
 			}
 
@@ -174,12 +175,15 @@ namespace Spine.Unity.Editor {
 			// Header
 			EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(target.name + " (SkeletonDataAsset)", Icons.spine), EditorStyles.whiteLargeLabel);
 			if (targetSkeletonData != null) EditorGUILayout.LabelField("(Drag and Drop to instantiate.)", EditorStyles.miniLabel);
-
+			
 			// Main Serialized Fields
 			using (var changeCheck = new EditorGUI.ChangeCheckScope()) {
 				using (new SpineInspectorUtility.BoxScope())
 					DrawSkeletonDataFields();
 
+				if (compatibilityProblemInfo != null)
+					return;
+
 				using (new SpineInspectorUtility.BoxScope()) {
 					DrawAtlasAssetsFields();
 					HandleAtlasAssetsNulls();
@@ -197,11 +201,11 @@ namespace Spine.Unity.Editor {
 					}
 				}
 			}
-
+			
 			// Unity Quirk: Some code depends on valid preview. If preview is initialized elsewhere, this can cause contents to change between Layout and Repaint events, causing GUILayout control count errors.
-			if (warnings.Count <= 0)
+			if (NoProblems())
 				preview.Initialize(this.Repaint, targetSkeletonDataAsset, this.LastSkinName);
-
+			
 			if (targetSkeletonData != null) {
 				GUILayout.Space(20f);
 
@@ -317,6 +321,12 @@ namespace Spine.Unity.Editor {
 				}
 			}
 			EditorGUILayout.PropertyField(skeletonJSON, SpineInspectorUtility.TempContent(skeletonJSON.displayName, Icons.spine));
+
+			if (compatibilityProblemInfo != null) {
+				EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(compatibilityProblemInfo.DescriptionString(), Icons.warning), GUILayout.Height(52));
+				return;
+			}
+
 			EditorGUILayout.DelayedFloatField(scale); //EditorGUILayout.PropertyField(scale);
 			EditorGUILayout.Space();
 			EditorGUILayout.PropertyField(skeletonDataModifiers, true);
@@ -581,12 +591,14 @@ namespace Spine.Unity.Editor {
 
 		void PopulateWarnings () {
 			warnings.Clear();
+			compatibilityProblemInfo = null;
 
 			if (skeletonJSON.objectReferenceValue == null) {
 				warnings.Add("Missing Skeleton JSON");
 			} else {
 				var fieldValue = (TextAsset)skeletonJSON.objectReferenceValue;
-				if (!AssetUtility.IsSpineData(fieldValue)) {
+				
+				if (!AssetUtility.IsSpineData(fieldValue, out compatibilityProblemInfo)) {
 					warnings.Add("Skeleton data file is not a valid Spine JSON or binary file.");
 				} else {
 					#if SPINE_TK2D
@@ -657,6 +669,10 @@ namespace Spine.Unity.Editor {
 			EditorPrefs.SetString(LastSkinKey, skinName);
 		}
 
+		bool NoProblems() {
+			return warnings.Count == 0 && compatibilityProblemInfo == null;
+		}
+
 		#region Preview Handlers
 		void HandleOnDestroyPreview () {
 			EditorApplication.update -= preview.HandleEditorUpdate;
@@ -677,7 +693,7 @@ namespace Spine.Unity.Editor {
 		}
 
 		override public void OnInteractivePreviewGUI (Rect r, GUIStyle background) {
-			if (warnings.Count <= 0) {
+			if (NoProblems()) {
 				preview.Initialize(this.Repaint, targetSkeletonDataAsset, this.LastSkinName);
 				preview.HandleInteractivePreviewGUI(r, background);
 			}

+ 63 - 88
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/AssetUtility.cs

@@ -46,18 +46,17 @@ using System.IO;
 using System.Text;
 using System.Linq;
 using System.Reflection;
-using System.Globalization;
+
+using CompatibilityProblemInfo = Spine.Unity.SkeletonDataCompatibility.CompatibilityProblemInfo;
 
 namespace Spine.Unity.Editor {
+	using PathAndProblemInfo = System.Collections.Generic.KeyValuePair<string, CompatibilityProblemInfo>;
 
 	public static class AssetUtility {
+
 		public const string SkeletonDataSuffix = "_SkeletonData";
 		public const string AtlasSuffix = "_Atlas";
 
-		static readonly int[][] compatibleBinaryVersions = { new[] { 3, 8, 0 } };
-		static readonly int[][] compatibleJsonVersions = { new[] { 3, 8, 0 } };
-		//static bool isFixVersionRequired = false;
-
 		/// HACK: This list keeps the asset reference temporarily during importing.
 		///
 		/// In cases of very large projects/sufficient RAM pressure, when AssetDatabase.SaveAssets is called,
@@ -125,7 +124,11 @@ namespace Spine.Unity.Editor {
 			if (root == null || !root.ContainsKey("skins"))
 				return requiredPaths;
 
-			foreach (Dictionary<string, object> skinMap in (List<object>)root["skins"]) {
+			var skinsList = root["skins"] as List<object>;
+			if (skinsList == null)
+				return requiredPaths;
+
+			foreach (Dictionary<string, object> skinMap in skinsList) {
 				if (!skinMap.ContainsKey("attachments"))
 					continue;
 				foreach (var slot in (Dictionary<string, object>)skinMap["attachments"]) {
@@ -243,7 +246,8 @@ namespace Spine.Unity.Editor {
 		public static void ImportSpineContent (string[] imported, bool reimport = false) {
 			var atlasPaths = new List<string>();
 			var imagePaths = new List<string>();
-			var skeletonPaths = new List<string>();
+			var skeletonPaths = new List<PathAndProblemInfo>();
+			CompatibilityProblemInfo compatibilityProblemInfo = null;
 
 			foreach (string str in imported) {
 				string extension = Path.GetExtension(str).ToLower();
@@ -263,13 +267,13 @@ namespace Spine.Unity.Editor {
 						break;
 					case ".json":
 						var jsonAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(str);
-						if (jsonAsset != null && IsSpineData(jsonAsset))
-							skeletonPaths.Add(str);
+						if (jsonAsset != null && IsSpineData(jsonAsset, out compatibilityProblemInfo))
+							skeletonPaths.Add(new PathAndProblemInfo(str, compatibilityProblemInfo));
 						break;
 					case ".bytes":
 						if (str.ToLower().EndsWith(".skel.bytes", System.StringComparison.Ordinal)) {
-							if (IsSpineData(AssetDatabase.LoadAssetAtPath<TextAsset>(str)))
-								skeletonPaths.Add(str);
+							if (IsSpineData(AssetDatabase.LoadAssetAtPath<TextAsset>(str), out compatibilityProblemInfo))
+								skeletonPaths.Add(new PathAndProblemInfo(str, compatibilityProblemInfo));
 						}
 						break;
 				}
@@ -287,24 +291,32 @@ namespace Spine.Unity.Editor {
 
 			// Import skeletons and match them with atlases.
 			bool abortSkeletonImport = false;
-			foreach (string skeletonPath in skeletonPaths) {
+			foreach (var skeletonPathEntry in skeletonPaths) {
+				string skeletonPath = skeletonPathEntry.Key;
+				var compatibilityProblems = skeletonPathEntry.Value;
 				if (skeletonPath.StartsWith("Packages"))
 					continue;
 				if (!reimport && CheckForValidSkeletonData(skeletonPath)) {
-					ReloadSkeletonData(skeletonPath);
+					ReloadSkeletonData(skeletonPath, compatibilityProblems);
+					continue;
+				}
+
+				var loadedAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(skeletonPath);
+				if (compatibilityProblems != null) {
+					IngestIncompatibleSpineProject(loadedAsset, compatibilityProblems);
 					continue;
 				}
 
 				string dir = Path.GetDirectoryName(skeletonPath);
 
 #if SPINE_TK2D
-				IngestSpineProject(AssetDatabase.LoadAssetAtPath<TextAsset>(skeletonPath), null);
+				IngestSpineProject(loadedAsset, compatibilityProblems, null);
 #else
 				var localAtlases = FindAtlasesAtPath(dir);
 				var requiredPaths = GetRequiredAtlasRegions(skeletonPath);
 				var atlasMatch = GetMatchingAtlas(requiredPaths, localAtlases);
 				if (atlasMatch != null || requiredPaths.Count == 0) {
-					IngestSpineProject(AssetDatabase.LoadAssetAtPath<TextAsset>(skeletonPath), atlasMatch);
+					IngestSpineProject(loadedAsset, atlasMatch);
 				} else {
 					SkeletonImportDialog(skeletonPath, localAtlases, requiredPaths, ref abortSkeletonImport);
 				}
@@ -338,7 +350,7 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
-		static void ReloadSkeletonData (string skeletonJSONPath) {
+		static void ReloadSkeletonData (string skeletonJSONPath, CompatibilityProblemInfo compatibilityProblemInfo) {
 			string dir = Path.GetDirectoryName(skeletonJSONPath);
 			TextAsset textAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(skeletonJSONPath);
 			DirectoryInfo dirInfo = new DirectoryInfo(dir);
@@ -353,6 +365,11 @@ namespace Spine.Unity.Editor {
 						if (Selection.activeObject == skeletonDataAsset)
 							Selection.activeObject = null;
 
+						if (compatibilityProblemInfo != null) {
+							SkeletonDataCompatibility.DisplayCompatibilityProblem(compatibilityProblemInfo.DescriptionString(), textAsset);
+							return;
+						}
+
 						Debug.LogFormat("Changes to '{0}' detected. Clearing SkeletonDataAsset: {1}", skeletonJSONPath, localPath);
 						skeletonDataAsset.Clear();
 
@@ -545,10 +562,34 @@ namespace Spine.Unity.Editor {
 #endregion
 
 #region Import SkeletonData (json or binary)
-		internal static SkeletonDataAsset IngestSpineProject (TextAsset spineJson, params AtlasAssetBase[] atlasAssets) {
+		internal static string GetSkeletonDataAssetFilePath(TextAsset spineJson) {
 			string primaryName = Path.GetFileNameWithoutExtension(spineJson.name);
 			string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson));
-			string filePath = assetPath + "/" + primaryName + SkeletonDataSuffix + ".asset";
+			return assetPath + "/" + primaryName + SkeletonDataSuffix + ".asset";
+		}
+
+		internal static SkeletonDataAsset IngestIncompatibleSpineProject(TextAsset spineJson,
+			CompatibilityProblemInfo compatibilityProblemInfo) {
+
+			string filePath = GetSkeletonDataAssetFilePath(spineJson);
+
+			if (spineJson == null)
+				return null;
+
+			SkeletonDataAsset skeletonDataAsset = (SkeletonDataAsset)AssetDatabase.LoadAssetAtPath(filePath, typeof(SkeletonDataAsset));
+			if (skeletonDataAsset == null) {
+				skeletonDataAsset = SkeletonDataAsset.CreateInstance<SkeletonDataAsset>();
+				skeletonDataAsset.skeletonJSON = spineJson;
+				AssetDatabase.CreateAsset(skeletonDataAsset, filePath);
+			}
+			EditorUtility.SetDirty(skeletonDataAsset);
+			
+			SkeletonDataCompatibility.DisplayCompatibilityProblem(compatibilityProblemInfo.DescriptionString(), spineJson);
+			return skeletonDataAsset;
+		}
+
+		internal static SkeletonDataAsset IngestSpineProject (TextAsset spineJson, params AtlasAssetBase[] atlasAssets) {
+			string filePath = GetSkeletonDataAssetFilePath(spineJson);
 
 #if SPINE_TK2D
 			if (spineJson != null) {
@@ -622,76 +663,10 @@ namespace Spine.Unity.Editor {
 			return false;
 		}
 
-		public static bool IsSpineData (TextAsset asset) {
-			if (asset == null)
-				return false;
-
-			bool isSpineData = false;
-			string rawVersion = null;
-
-			int[][] compatibleVersions;
-			if (asset.name.Contains(".skel")) {
-				try {
-					using (var memStream = new MemoryStream(asset.bytes)) {
-						rawVersion = SkeletonBinary.GetVersionString(memStream);
-					}
-					isSpineData = !(string.IsNullOrEmpty(rawVersion));
-					compatibleVersions = compatibleBinaryVersions;
-				} catch (System.Exception e) {
-					Debug.LogErrorFormat("Failed to read '{0}'. It is likely not a binary Spine SkeletonData file.\n{1}", asset.name, e);
-					return false;
-				}
-			} else {
-				object obj = Json.Deserialize(new StringReader(asset.text));
-				if (obj == null) {
-					Debug.LogErrorFormat("'{0}' is not valid JSON.", asset.name);
-					return false;
-				}
-
-				var root = obj as Dictionary<string, object>;
-				if (root == null) {
-					Debug.LogError("Parser returned an incorrect type.");
-					return false;
-				}
-
-				isSpineData = root.ContainsKey("skeleton");
-				if (isSpineData) {
-					var skeletonInfo = (Dictionary<string, object>)root["skeleton"];
-					object jv;
-					skeletonInfo.TryGetValue("spine", out jv);
-					rawVersion = jv as string;
-				}
-
-				compatibleVersions = compatibleJsonVersions;
-			}
-
-			// Version warning
-			if (isSpineData) {
-				string primaryRuntimeVersionDebugString = compatibleVersions[0][0] + "." + compatibleVersions[0][1];
-
-				if (string.IsNullOrEmpty(rawVersion)) {
-					Debug.LogWarningFormat("Skeleton '{0}' has no version information. It may be incompatible with your runtime version: spine-unity v{1}", asset.name, primaryRuntimeVersionDebugString);
-				} else {
-					string[] versionSplit = rawVersion.Split('.');
-					bool match = false;
-					foreach (var version in compatibleVersions) {
-						bool primaryMatch = version[0] == int.Parse(versionSplit[0], CultureInfo.InvariantCulture);
-						bool secondaryMatch = version[1] == int.Parse(versionSplit[1], CultureInfo.InvariantCulture);
-
-						// if (isFixVersionRequired) secondaryMatch &= version[2] <= int.Parse(jsonVersionSplit[2], CultureInfo.InvariantCulture);
-
-						if (primaryMatch && secondaryMatch) {
-							match = true;
-							break;
-						}
-					}
-
-					if (!match)
-						Debug.LogWarningFormat("Skeleton '{0}' (exported with Spine {1}) may be incompatible with your runtime version: spine-csharp v{2}", asset.name, rawVersion, primaryRuntimeVersionDebugString);
-				}
-			}
-
-			return isSpineData;
+		public static bool IsSpineData (TextAsset asset, out CompatibilityProblemInfo compatibilityProblemInfo) {
+			SkeletonDataCompatibility.VersionInfo fileVersion = SkeletonDataCompatibility.GetVersionInfo(asset);
+			compatibilityProblemInfo = SkeletonDataCompatibility.GetCompatibilityProblemInfo(fileVersion);
+			return fileVersion != null;
 		}
 #endregion
 

+ 31 - 20
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataAsset.cs

@@ -32,7 +32,7 @@ using System.Collections.Generic;
 using System.IO;
 using UnityEngine;
 
-using Spine;
+using CompatibilityProblemInfo = Spine.Unity.SkeletonDataCompatibility.CompatibilityProblemInfo;
 
 namespace Spine.Unity {
 
@@ -114,26 +114,26 @@ namespace Spine.Unity {
 				Clear();
 				return null;
 			}
-
+			
 			// Disabled to support attachmentless/skinless SkeletonData.
-//			if (atlasAssets == null) {
-//				atlasAssets = new AtlasAsset[0];
-//				if (!quiet)
-//					Debug.LogError("Atlas not set for SkeletonData asset: " + name, this);
-//				Clear();
-//				return null;
-//			}
-//			#if !SPINE_TK2D
-//			if (atlasAssets.Length == 0) {
-//				Clear();
-//				return null;
-//			}
-//			#else
-//			if (atlasAssets.Length == 0 && spriteCollection == null) {
-//				Clear();
-//				return null;
-//			}
-//			#endif
+			//			if (atlasAssets == null) {
+			//				atlasAssets = new AtlasAsset[0];
+			//				if (!quiet)
+			//					Debug.LogError("Atlas not set for SkeletonData asset: " + name, this);
+			//				Clear();
+			//				return null;
+			//			}
+			//			#if !SPINE_TK2D
+			//			if (atlasAssets.Length == 0) {
+			//				Clear();
+			//				return null;
+			//			}
+			//			#else
+			//			if (atlasAssets.Length == 0 && spriteCollection == null) {
+			//				Clear();
+			//				return null;
+			//			}
+			//			#endif
 
 			if (skeletonData != null)
 				return skeletonData;
@@ -163,6 +163,17 @@ namespace Spine.Unity {
 			bool isBinary = skeletonJSON.name.ToLower().Contains(".skel");
 			SkeletonData loadedSkeletonData;
 
+			#if UNITY_EDITOR
+			if (skeletonJSON) {
+				SkeletonDataCompatibility.VersionInfo fileVersion = SkeletonDataCompatibility.GetVersionInfo(skeletonJSON);
+				CompatibilityProblemInfo compatibilityProblemInfo = SkeletonDataCompatibility.GetCompatibilityProblemInfo(fileVersion);
+				if (compatibilityProblemInfo != null) {
+					SkeletonDataCompatibility.DisplayCompatibilityProblem(compatibilityProblemInfo.DescriptionString(), skeletonJSON);
+					return null;
+				}
+			}
+			#endif
+
 			try {
 				if (isBinary)
 					loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(skeletonJSON.bytes, attachmentLoader, skeletonDataScale);

+ 160 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataCompatibility.cs

@@ -0,0 +1,160 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated May 1, 2019. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2019, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS
+ * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System.Collections.Generic;
+using System.IO;
+using UnityEngine;
+using System.Globalization;
+
+namespace Spine.Unity {
+
+	public static class SkeletonDataCompatibility {
+
+		static readonly int[][] compatibleBinaryVersions = { new[] { 3, 8, 0 } };
+		static readonly int[][] compatibleJsonVersions = { new[] { 3, 8, 0 } };
+
+		static bool wasVersionDialogShown = false;
+
+		public enum SourceType {
+			Json,
+			Binary
+		}
+
+		[System.Serializable]
+		public class VersionInfo {
+			public string rawVersion = null;
+			public int[] version = null;
+			public SourceType sourceType;
+		}
+
+		[System.Serializable]
+		public class CompatibilityProblemInfo {
+			public VersionInfo actualVersion;
+			public int[][] compatibleVersions;
+
+			public string DescriptionString () {
+				string compatibleVersionString = "";
+				string optionalOr = null;
+				foreach (int[] version in compatibleVersions) {
+					compatibleVersionString += string.Format("{0}{1}.{2}", optionalOr, version[0], version[1]);
+					optionalOr = " or ";
+				}
+				return string.Format("Skeleton data could not be loaded. Data version: {0}. Required version: {1}.\nPlease re-export skeleton data with Spine {1} or change runtime to version {2}.{3}.",
+					actualVersion.rawVersion, compatibleVersionString, actualVersion.version[0], actualVersion.version[1]);
+			}
+		}
+
+	#if UNITY_EDITOR
+		public static VersionInfo GetVersionInfo (TextAsset asset) {
+			if (asset == null)
+				return null;
+
+			VersionInfo fileVersion = new VersionInfo();
+			fileVersion.sourceType = asset.name.Contains(".skel") ? SourceType.Binary : SourceType.Json;
+
+			if (fileVersion.sourceType == SourceType.Binary) {
+				try {
+					using (var memStream = new MemoryStream(asset.bytes)) {
+						fileVersion.rawVersion = SkeletonBinary.GetVersionString(memStream);
+					}
+				}
+				catch (System.Exception e) {
+					Debug.LogErrorFormat("Failed to read '{0}'. It is likely not a binary Spine SkeletonData file.\n{1}", asset.name, e);
+					return null;
+				}
+			}
+			else {
+				object obj = Json.Deserialize(new StringReader(asset.text));
+				if (obj == null) {
+					Debug.LogErrorFormat("'{0}' is not valid JSON.", asset.name);
+					return null;
+				}
+
+				var root = obj as Dictionary<string, object>;
+				if (root == null) {
+					Debug.LogErrorFormat("'{0}' is not compatible JSON. Parser returned an incorrect type while parsing version info.", asset.name);
+					return null;
+				}
+
+				if (root.ContainsKey("skeleton")) {
+					var skeletonInfo = (Dictionary<string, object>)root["skeleton"];
+					object jv;
+					skeletonInfo.TryGetValue("spine", out jv);
+					fileVersion.rawVersion = jv as string;
+				}
+			}
+
+			string primaryRuntimeVersionDebugString = compatibleBinaryVersions[0][0] + "." + compatibleBinaryVersions[0][1];
+			if (string.IsNullOrEmpty(fileVersion.rawVersion)) {
+				Debug.LogWarningFormat("Skeleton '{0}' has no version information. It is incompatible with your runtime version: spine-unity v{1}", asset.name, primaryRuntimeVersionDebugString);
+				return null;
+			}
+
+			var versionSplit = fileVersion.rawVersion.Split('.');
+			try {
+				fileVersion.version = new[]{ int.Parse(versionSplit[0], CultureInfo.InvariantCulture),
+									int.Parse(versionSplit[1], CultureInfo.InvariantCulture) };
+			}
+			catch (System.Exception e) {
+				Debug.LogErrorFormat("Failed to read version info at skeleton '{0}'. It is likely not a valid Spine SkeletonData file.\n{1}", asset.name, e);
+				return null;
+			}
+			return fileVersion;
+		}
+
+		public static CompatibilityProblemInfo GetCompatibilityProblemInfo (VersionInfo fileVersion) {
+			CompatibilityProblemInfo info = new CompatibilityProblemInfo();
+			info.actualVersion = fileVersion;
+			info.compatibleVersions = (fileVersion.sourceType == SourceType.Binary) ? compatibleBinaryVersions
+				: compatibleJsonVersions;
+
+			if (fileVersion == null)
+				return info;
+
+			foreach (var compatibleVersion in info.compatibleVersions) {
+				bool majorMatch = fileVersion.version[0] == compatibleVersion[0];
+				bool minorMatch = fileVersion.version[1] == compatibleVersion[1];
+				if (majorMatch && minorMatch) {
+					return null; // is compatible, thus no problem info returned
+				}
+			}
+			return info;
+		}
+
+		public static void DisplayCompatibilityProblem (string descriptionString, TextAsset spineJson) {
+			if (!wasVersionDialogShown) {
+				wasVersionDialogShown = true;
+				UnityEditor.EditorUtility.DisplayDialog("Version mismatch!", descriptionString, "OK");
+			}
+			Debug.LogError(string.Format("Error importing skeleton '{0}': {1}",
+				spineJson.name, descriptionString), spineJson);
+		}
+	#endif // UNITY_EDITOR
+	}
+}

+ 12 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataCompatibility.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4224df6e20549f0449154531ae080201
+timeCreated: 1567002861
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: