diff --git a/com.unity.multiplayer.mlapi-patcher/CHANGELOG.md b/com.unity.multiplayer.mlapi-patcher/CHANGELOG.md new file mode 100644 index 0000000..d0c8777 --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog +All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) + +## 1.0.0 +First version of the MLAPI Patcher Package \ No newline at end of file diff --git a/com.unity.multiplayer.mlapi-patcher/CHANGELOG.md.meta b/com.unity.multiplayer.mlapi-patcher/CHANGELOG.md.meta new file mode 100644 index 0000000..2a951d3 --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1c5bf7bc01b8d70459b5e5a4106e73bb +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.multiplayer.mlapi-patcher/Editor.meta b/com.unity.multiplayer.mlapi-patcher/Editor.meta new file mode 100644 index 0000000..4c2d78f --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 89cccfc12150d4f41b9e13fdb96d300f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.multiplayer.mlapi-patcher/Editor/MlapiPatcher.cs b/com.unity.multiplayer.mlapi-patcher/Editor/MlapiPatcher.cs new file mode 100644 index 0000000..f30fb07 --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/Editor/MlapiPatcher.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEditor.Build.Pipeline.Utilities; +using UnityEngine; +using UnityEngine.Assertions; +using Object = UnityEngine.Object; + +namespace MLAPI.Patcher.Editor +{ + public class MlapiPatcher : EditorWindow + { + const string k_DllMappingCachePath = "mlapi-patcher-dll-guids.temp.json"; + const string k_PatchStatePath = "mlapi-patcher-state.temp.json"; + + // This is class Id for MonoScript * 100'000 https://docs.unity3d.com/Manual/ClassIDReference.html + const string k_MissingScriptId = "11500000"; + + static readonly Dictionary k_APIChanges = new Dictionary() + { + { "NetworkingManager", "NetworkManager" }, + { "NetworkedObject", "NetworkObject" }, + { "NetworkedBehaviour", "NetworkBehaviour" }, + { "NetworkedClient", "NetworkClient" }, + { "NetworkedPrefab", "NetworkPrefab" }, + { "NetworkedVar", "NetworkVariable" }, + { "NetworkedTransform", "NetworkTransform" }, + { "NetworkedAnimator", "NetworkAnimator" }, + { "NetworkedAnimatorEditor", "NetworkAnimatorEditor" }, + { "NetworkedNavMeshAgent", "NetworkNavMeshAgent" }, + { "SpawnManager", "NetworkSpawnManager" }, + { "BitStream", "NetworkBuffer" }, + { "PooledBitStream", "PooledNetworkBuffer" }, + { "BitSerializer", "NetworkSerializer" }, + { "BitReader", "NetworkReader" }, + { "BitWriter", "NetworkWriter" }, + { "PooledBitWriter", "PooledNetworkWriter" }, + { "PooledBitReader", "PooledNetworkReader" }, + { "NetEventType", "NetworkEventType" }, + { "ChannelType", "NetworkDelivery" }, + { "Channel", "NetworkChannel" }, + { "SendChannel", "SendNetworkChannel" }, + { "Transport", "NetworkTransport" }, + { "NetworkedDictionary", "NetworkDictionary" }, + { "NetworkedList", "NetworkList" }, + { "NetworkedSet", "NetworkSet" }, + { "MLAPIConstants", "NetworkConstants" }, + { "UnetTransport", "UNetTransport" }, + { "ServerRPC", "ServerRpc" }, + { "ClientRPC", "ClientRpc" }, + }; + + static readonly List OldMlapiUnityObjects = new List() + { + "NetworkingManager", + "NetworkedObject", + "NetworkedTransform", + "NetworkedAnimator", + "NetworkedNavMeshAgent", + "UnetTransport", + }; + + [MenuItem("Window/MLAPI Patcher")] + public static void ShowWindow() => GetWindow(typeof(MlapiPatcher)); + + bool? m_DllVersion = null; + + Object m_SourceVersionDirectory; + + void OnEnable() + { + titleContent.text = "MLAPI Patcher"; + } + + void OnGUI() + { + if (m_DllVersion == null) + { + GUILayout.Label("Are you using the installer or the source version of MLAPI?"); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Installer")) + { + m_DllVersion = true; + } + + if (GUILayout.Button("Source")) + { + m_DllVersion = false; + } + + GUILayout.EndHorizontal(); + } + else + { + if (m_DllVersion.Value == false) + { + GUILayout.Label("MLAPI Source Directory"); + m_SourceVersionDirectory = EditorGUILayout.ObjectField(m_SourceVersionDirectory, typeof(Object), false); + } + + if (GUILayout.Button("Update Script References")) + { + ReplaceAllScriptReferences(m_DllVersion.Value); + } + + if (GUILayout.Button("Replace Type Names (Optional)")) + { + UpdateApiUsages(); + } + } + } + + private string FindMlapiDllPath() + { + var result = new List(); + FindFilesOfTypes(Application.dataPath, new[] { "MLAPI.dll" }, result); + if (result.Any()) + { + Assert.IsTrue(result.Count == 1); + return result.First(); + } + + return null; + } + + /// + /// References to a monobehaviour in a dll are stored as guid of the dll and fileID based on type. + /// This creates a table from guid => type name. + /// + private Dictionary BuildDllMapping() + { + var dllGuidToFileId = new Dictionary(); + + Assembly assembly = Assembly.LoadFrom(FindMlapiDllPath()); + + foreach (Type t in assembly.GetTypes()) + { + var fileId = ComputeGuid(t).ToString(); + if (dllGuidToFileId.ContainsKey(fileId)) + { + Debug.LogWarning($"duplicate guid: {fileId}, script name:{t.Name}"); + } + else + { + dllGuidToFileId[fileId] = t.Name; + + // Debug.Log($"found {fileId}: {t.Name}"); + } + } + + return dllGuidToFileId; + } + + /// + /// Builds a table which maps from + /// + /// + private Dictionary BuildSourceMapping() + { + Assert.IsNotNull(m_SourceVersionDirectory); + + var sourceMapping = new Dictionary(); + + var filePaths = new List(); + var folderPath = AssetDatabase.GetAssetPath(m_SourceVersionDirectory); + var fileNames = OldMlapiUnityObjects.Select(t => t + ".cs.meta").ToArray(); + + FindFilesOfTypes(folderPath, fileNames, filePaths); + + Assert.IsTrue(fileNames.Length == OldMlapiUnityObjects.Count); + + foreach (var path in filePaths) + { + sourceMapping[ExtractGuidFromMetaFile(path)] = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(path)); + } + + return sourceMapping; + } + + Dictionary BuildPackageGuidMapping() + { + var packageTypeToGuid = new Dictionary(); + + var metaTypes = new[] { ".cs.meta" }; + var filePaths = new List(); + + FindFilesOfTypes(GetMlapiPackageFolderPath(), metaTypes, filePaths); + + foreach (string path in filePaths) + { + packageTypeToGuid[Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(path))] = ExtractGuidFromMetaFile(path); + } + + return packageTypeToGuid; + } + + string GetMlapiPackageFolderPath() + { + var assetPath = Application.dataPath; + var parentDirectory = new DirectoryInfo(assetPath).Parent; + Assert.IsNotNull(parentDirectory); + + var packageCacheFolderPath = Path.Combine(parentDirectory.FullName, Path.Combine(Path.Combine("Library", "PackageCache"))); + var directory = new DirectoryInfo(packageCacheFolderPath); + var mlapiPackageFolder = directory.GetDirectories().First(t => t.Name.StartsWith("com.unity.multiplayer.mlapi@")); + + Assert.IsNotNull(mlapiPackageFolder); + + return mlapiPackageFolder.FullName; + } + + /// + /// Recursively finds all the files ending with the given types. + /// + /// The path to collect the files from. Can be a folder or a single file. + /// The file endings to collect. + /// The list to add the found results. + private void FindFilesOfTypes(string path, string[] types, List results) + { + if (File.Exists(path)) + { + results.Add(path); + } + else + { + if (!string.IsNullOrEmpty(path)) + { + foreach (string file in Directory.GetFiles(path)) + { + foreach (string type in types) + { + if (file.EndsWith(type)) + { + results.Add(file); + break; + } + } + } + + foreach (string directory in Directory.GetDirectories(path)) + { + FindFilesOfTypes(directory, types, results); + } + } + } + } + + private string ExtractGuidFromMetaFile(string filePath) + { + using (StreamReader streamReader = new StreamReader(filePath)) + { + while (!streamReader.EndOfStream) + { + var line = streamReader.ReadLine(); + if (line.StartsWith("guid:")) + { + return line.Substring(line.IndexOf(":") + 2); + } + } + } + + throw new InvalidOperationException($"guid not found in file: {filePath}"); + } + + private void UpdateApiUsages() + { + var results = new List(); + FindFilesOfTypes(Application.dataPath, new[] { ".cs" }, results); + + Dictionary replacements = new Dictionary(); + foreach (var apiChange in k_APIChanges) + { + var regex = new Regex($"(? |\\.|<|\\[|\\(|!){apiChange.Key}(?!(s.UNET))"); + var replacement = $"${{prefix}}{apiChange.Value}"; + replacements.Add(regex, replacement); + } + + for (int i = 0; i < results.Count; i++) + { + EditorUtility.DisplayProgressBar("Update type names", results[i], (float)i / results.Count); + UpdateApiUsagesForFile(results[i], replacements); + } + + AssetDatabase.Refresh(); + EditorUtility.ClearProgressBar(); + } + + private void UpdateApiUsagesForFile(string filePath, Dictionary replacements) + { + string[] lines = File.ReadAllLines(filePath); + + bool replacedAny = false; + for (int i = 0; i < lines.Length; i++) + { + foreach (var replacement in replacements) + { + //var newLine = lines[i].Replace($" {apiChange.Key}", $" {apiChange.Value}"); + + var newLine = replacement.Key.Replace(lines[i], replacement.Value); + + if (newLine != lines[i]) + { + replacedAny = true; + lines[i] = newLine; + } + } + } + + if (replacedAny) + { + Debug.Log($"Updated APIs in file {filePath}"); + File.WriteAllLines(filePath, lines); + } + } + + private void ReplaceAllScriptReferences(bool fromDllVersion) + { + Dictionary initialMapping; + if (fromDllVersion) + { + initialMapping = BuildDllMapping(); + } + else + { + initialMapping = BuildSourceMapping(); + } + + var packageMapping = BuildPackageGuidMapping(); + + var relevantObjectTypes = new string[3] { ".asset", ".prefab", ".unity" }; + var results = new List(); + FindFilesOfTypes(Application.dataPath, relevantObjectTypes, results); + + for (int i = 0; i < results.Count; i++) + { + EditorUtility.DisplayProgressBar("Update Script References", results[i], (float)i / results.Count); + ReplaceScriptReferencesForFile(results[i], initialMapping, packageMapping, fromDllVersion); + } + + File.Delete(Path.Combine(Application.dataPath, k_DllMappingCachePath)); + + AssetDatabase.Refresh(); + EditorUtility.ClearProgressBar(); + } + + private void ReplaceScriptReferencesForFile(string filePath, Dictionary initialMapping, Dictionary packageMapping, bool fromDllVersion) + { + string[] lines = File.ReadAllLines(filePath); + + bool replacedAny = false; + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i]; + if (line.StartsWith("MonoBehaviour:")) + { + while (!lines[i].TrimStart().StartsWith("m_Script:")) + { + i++; + } + + if (ReplaceGuidsInLine(ref lines[i], initialMapping, packageMapping, fromDllVersion)) + { + replacedAny = true; + } + } + } + + if (replacedAny) + { + File.WriteAllLines(filePath, lines); + } + } + + /// + /// Replaces the guids in the given line string. + /// + /// The line. + /// True if a replacement was done else false/ + private bool ReplaceGuidsInLine(ref string line, Dictionary initialMapping, Dictionary packageMapping, bool fromDllVersion) + { + string fileId = ExtractFromLine(line, "fileID"); + string guid = ExtractFromLine(line, "guid"); + + Assert.IsNotNull(guid); + + if (fromDllVersion) + { + if (fileId == null || fileId == k_MissingScriptId) + { + return false; + } + } + + var key = fromDllVersion ? fileId : guid; + + if (initialMapping.TryGetValue(key, out string originalName)) + { + var updatedName = UpdateTypeName(originalName); + if (packageMapping.TryGetValue(updatedName, out string newValue)) + { + Debug.Log("Replace reference:" + originalName); + + line = line.Replace(guid, newValue); + line = line.Replace(fileId, "11500000"); + return true; + } + + Debug.LogWarning($"Can't find guid of file: {originalName}"); + } + + return false; + } + + private string UpdateTypeName(string oldTypeName) + { + if (k_APIChanges.TryGetValue(oldTypeName, out string newTypeName)) + { + return newTypeName; + } + + return oldTypeName; + } + + private static string ExtractFromLine(string line, string identifier) + { + int start = line.IndexOf($"{identifier}:") + $"{identifier}: ".Length; + int lenght = line.IndexOf(",", start) - start; + return lenght > 0 ? line.Substring(start, lenght) : null; + } + + private static int ComputeGuid(Type t) // TODO why does scriptable build pipeline not provide this + { + string hashGenerator = "s\0\0\0" + t.Namespace + t.Name; + using (var md4 = MD4.Create()) + { + byte[] hash = md4.ComputeHash(Encoding.UTF8.GetBytes(hashGenerator)); + return BitConverter.ToInt32(hash, 0); + } + } + } +} diff --git a/com.unity.multiplayer.mlapi-patcher/Editor/MlapiPatcher.cs.meta b/com.unity.multiplayer.mlapi-patcher/Editor/MlapiPatcher.cs.meta new file mode 100644 index 0000000..7f0de20 --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/Editor/MlapiPatcher.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8340a0eccd2a5f4ba43c011bf9f5e3a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.multiplayer.mlapi-patcher/Editor/com.unity.multiplayer.mlapi-patcher.asmdef b/com.unity.multiplayer.mlapi-patcher/Editor/com.unity.multiplayer.mlapi-patcher.asmdef new file mode 100644 index 0000000..680f29f --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/Editor/com.unity.multiplayer.mlapi-patcher.asmdef @@ -0,0 +1,18 @@ +{ + "name": "MLAPI Patcher Editor", + "rootNamespace": "MLAPI.Patcher.Editor", + "references": [ + "Unity.ScriptableBuildPipeline.Editor" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/com.unity.multiplayer.mlapi-patcher/Editor/com.unity.multiplayer.mlapi-patcher.asmdef.meta b/com.unity.multiplayer.mlapi-patcher/Editor/com.unity.multiplayer.mlapi-patcher.asmdef.meta new file mode 100644 index 0000000..666ed75 --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/Editor/com.unity.multiplayer.mlapi-patcher.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: bb6aadbb978e7b145b9e225505747c83 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.multiplayer.mlapi-patcher/LICENSE b/com.unity.multiplayer.mlapi-patcher/LICENSE new file mode 100644 index 0000000..ea4b008 --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2021 Unity Technologies + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/com.unity.multiplayer.mlapi-patcher/LICENSE.meta b/com.unity.multiplayer.mlapi-patcher/LICENSE.meta new file mode 100644 index 0000000..96ded3a --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9a6fb765e42eae348bb948a319b66419 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.multiplayer.mlapi-patcher/README.md b/com.unity.multiplayer.mlapi-patcher/README.md new file mode 100644 index 0000000..483ca8d --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/README.md @@ -0,0 +1,5 @@ +## MLAPI Patcher package + +This package exists to make upgrading from the previous MLAPI version (v12) to the new experimental Unity package version of MLAPI possible. This Patcher is meant to be used in combination with the [upgrade guide](https://docs-multiplayer.unity3d.com/docs/migration/migratingfrommlapi). + +Use the following git url in the package manager to install this package: https://github.com/Unity-Technologies/mlapi-community-contributions.git?path=/com.unity.multiplayer.mlapi-patcher diff --git a/com.unity.multiplayer.mlapi-patcher/README.md.meta b/com.unity.multiplayer.mlapi-patcher/README.md.meta new file mode 100644 index 0000000..9d5fea5 --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c57ec5067a76b5545beb53be11091e99 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.multiplayer.mlapi-patcher/package.json b/com.unity.multiplayer.mlapi-patcher/package.json new file mode 100644 index 0000000..3b7bf1d --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/package.json @@ -0,0 +1,11 @@ +{ + "name": "com.unity.multiplayer.mlapi-patcher", + "displayName": "MLAPI Patcher Package", + "version": "0.1.0", + "unity": "2019.4", + "description": "Package provides utility for upgrading MLAPI to the Unity Package Manager version.", + "author": "", + "dependencies": { + "com.unity.scriptablebuildpipeline" : "1.14.1" + } +} diff --git a/com.unity.multiplayer.mlapi-patcher/package.json.meta b/com.unity.multiplayer.mlapi-patcher/package.json.meta new file mode 100644 index 0000000..95f6827 --- /dev/null +++ b/com.unity.multiplayer.mlapi-patcher/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3bc7bdb9fdc2e7c4493f6667c4e3394d +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: