* feat: Add patcher package for updating MLAPI

* fix: Missing using declaration

* Improve naming

* Improve regex

* Cleanup structure and meta files

* Improve replacement regex

* Add link to upgrade guide in readme

* update asmdef :)
This commit is contained in:
Luke Stampfli 2021-03-23 16:04:05 +01:00 коммит произвёл GitHub
Родитель 89b57b6331
Коммит a7cfac0a9a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 550 добавлений и 0 удалений

Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1c5bf7bc01b8d70459b5e5a4106e73bb
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Просмотреть файл

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 89cccfc12150d4f41b9e13fdb96d300f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Просмотреть файл

@ -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<string, string> k_APIChanges = new Dictionary<string, string>()
{
{ "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<string> OldMlapiUnityObjects = new List<string>()
{
"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<string>();
FindFilesOfTypes(Application.dataPath, new[] { "MLAPI.dll" }, result);
if (result.Any())
{
Assert.IsTrue(result.Count == 1);
return result.First();
}
return null;
}
/// <summary>
/// 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.
/// </summary>
private Dictionary<string, string> BuildDllMapping()
{
var dllGuidToFileId = new Dictionary<string, string>();
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;
}
/// <summary>
/// Builds a table which maps from
/// </summary>
/// <returns></returns>
private Dictionary<string, string> BuildSourceMapping()
{
Assert.IsNotNull(m_SourceVersionDirectory);
var sourceMapping = new Dictionary<string, string>();
var filePaths = new List<string>();
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<string, string> BuildPackageGuidMapping()
{
var packageTypeToGuid = new Dictionary<string, string>();
var metaTypes = new[] { ".cs.meta" };
var filePaths = new List<string>();
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;
}
/// <summary>
/// Recursively finds all the files ending with the given types.
/// </summary>
/// <param name="path">The path to collect the files from. Can be a folder or a single file.</param>
/// <param name="types">The file endings to collect.</param>
/// <param name="results">The list to add the found results.</param>
private void FindFilesOfTypes(string path, string[] types, List<string> 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<string>();
FindFilesOfTypes(Application.dataPath, new[] { ".cs" }, results);
Dictionary<Regex, string> replacements = new Dictionary<Regex, string>();
foreach (var apiChange in k_APIChanges)
{
var regex = new Regex($"(?<prefix> |\\.|<|\\[|\\(|!){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<Regex, string> 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<string, string> initialMapping;
if (fromDllVersion)
{
initialMapping = BuildDllMapping();
}
else
{
initialMapping = BuildSourceMapping();
}
var packageMapping = BuildPackageGuidMapping();
var relevantObjectTypes = new string[3] { ".asset", ".prefab", ".unity" };
var results = new List<string>();
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<string, string> initialMapping, Dictionary<string, string> 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);
}
}
/// <summary>
/// Replaces the guids in the given line string.
/// </summary>
/// <param name="line">The line.</param>
/// <returns>True if a replacement was done else false/</returns>
private bool ReplaceGuidsInLine(ref string line, Dictionary<string, string> initialMapping, Dictionary<string, string> 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);
}
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d8340a0eccd2a5f4ba43c011bf9f5e3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bb6aadbb978e7b145b9e225505747c83
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9a6fb765e42eae348bb948a319b66419
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c57ec5067a76b5545beb53be11091e99
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3bc7bdb9fdc2e7c4493f6667c4e3394d
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: