[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.

This commit is contained in:
Harald Csaszar 2019-08-28 17:30:35 +02:00
parent 45362e60bf
commit 38d0204e72
5 changed files with 291 additions and 117 deletions

View File

@ -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);
}

View File

@ -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 }, new[] { 3, 9, 0 } };
static readonly int[][] compatibleJsonVersions = { new[] { 3, 8, 0 }, new[] { 3, 9, 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

View File

@ -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);

View File

@ -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, 9, 0 }, new[] { 3, 8, 0 } };
static readonly int[][] compatibleJsonVersions = { new[] { 3, 9, 0 }, 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
}
}

View File

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