// ---------------------------------------------------------------------------- // // PhotonNetwork Framework for Unity - Copyright (C) 2011 Exit Games GmbH // // // Script to convert a Unity Networking project to PhotonNetwork. // // developer@exitgames.com // ---------------------------------------------------------------------------- #if UNITY_5 && !UNITY_5_0 && !UNITY_5_1 && !UNITY_5_2 #define UNITY_MIN_5_3 #endif using UnityEditor.SceneManagement; using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; public class PhotonConverter : Photon.MonoBehaviour { public static void RunConversion() { //Ask if user has made a backup. int option = EditorUtility.DisplayDialogComplex("Conversion", "Attempt automatic conversion from Unity Networking to Photon Unity Networking \"PUN\"?", "Yes", "No!", "Pick Script Folder"); switch (option) { case 0: break; case 1: return; case 2: PickFolderAndConvertScripts(); return; default: return; } //REAAAALY? bool result = EditorUtility.DisplayDialog("Conversion", "Disclaimer: The code conversion feature is quite crude, but should do it's job well (see the sourcecode). A backup is therefore strongly recommended!", "Yes, I've made a backup: GO", "Abort"); if (!result) { return; } Output(EditorApplication.timeSinceStartup + " Started conversion of Unity networking -> Photon"); //Ask to save current scene (optional) EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); EditorUtility.DisplayProgressBar("Converting..", "Starting.", 0); //Convert NetworkViews to PhotonViews in Project prefabs //Ask the user if we can move all prefabs to a resources folder bool movePrefabs = EditorUtility.DisplayDialog("Conversion", "Can all prefabs that use a PhotonView be moved to a Resources/ folder? You need this if you use Network.Instantiate.", "Yes", "No"); string[] prefabs = Directory.GetFiles("Assets/", "*.prefab", SearchOption.AllDirectories); foreach (string prefab in prefabs) { EditorUtility.DisplayProgressBar("Converting..", "Object:" + prefab, 0.6f); Object[] objs = (Object[])AssetDatabase.LoadAllAssetsAtPath(prefab); int converted = 0; foreach (Object obj in objs) { if (obj != null && obj.GetType() == typeof(GameObject)) converted += ConvertNetworkView(((GameObject)obj).GetComponents(), false); } if (movePrefabs && converted > 0) { //This prefab needs to be under the root of a Resources folder! string path = prefab.Replace("\\", "/"); int lastSlash = path.LastIndexOf("/"); int resourcesIndex = path.LastIndexOf("/Resources/"); if (resourcesIndex != lastSlash - 10) { if (path.Contains("/Resources/")) { Debug.LogWarning("Warning, prefab [" + prefab + "] was already in a resources folder. But has been placed in the root of another one!"); } //This prefab NEEDS to be placed under a resources folder string resourcesFolder = path.Substring(0, lastSlash) + "/Resources/"; EnsureFolder(resourcesFolder); string newPath = resourcesFolder + path.Substring(lastSlash + 1); string error = AssetDatabase.MoveAsset(prefab, newPath); if (error != "") Debug.LogError(error); Output("Fixed prefab [" + prefab + "] by moving it into a resources folder."); } } } //Convert NetworkViews to PhotonViews in scenes string[] sceneFiles = Directory.GetFiles("Assets/", "*.unity", SearchOption.AllDirectories); foreach (string sceneName in sceneFiles) { EditorSceneManager.OpenScene(sceneName); EditorUtility.DisplayProgressBar("Converting..", "Scene:" + sceneName, 0.2f); int converted2 = ConvertNetworkView((NetworkView[])GameObject.FindObjectsOfType(typeof(NetworkView)), true); if (converted2 > 0) { //This will correct all prefabs: The prefabs have gotten new components, but the correct ID's were lost in this case PhotonViewHandler.HierarchyChange(); //TODO: most likely this is triggered on change or on save Output("Replaced " + converted2 + " NetworkViews with PhotonViews in scene: " + sceneName); EditorSceneManager.SaveOpenScenes(); } } //Convert C#/JS scripts (API stuff) List scripts = GetScriptsInFolder("Assets"); EditorUtility.DisplayProgressBar("Converting..", "Scripts..", 0.9f); ConvertScripts(scripts); Output(EditorApplication.timeSinceStartup + " Completed conversion!"); EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("Completed the conversion", "Don't forget to add \"PhotonNetwork.ConnectWithDefaultSettings();\" to connect to the Photon server before using any multiplayer functionality.", "OK"); } public static void PickFolderAndConvertScripts() { string folderPath = EditorUtility.OpenFolderPanel("Pick source folder to convert", Directory.GetCurrentDirectory(), ""); if (string.IsNullOrEmpty(folderPath)) { EditorUtility.DisplayDialog("Script Conversion", "No folder was selected. No files were changed. Please start over.", "Ok."); return; } bool result = EditorUtility.DisplayDialog("Script Conversion", "Scripts in this folder will be modified:\n\n" + folderPath + "\n\nMake sure you have backups of these scripts.\nConversion is not guaranteed to work!", "Backup done. Go!", "Abort"); if (!result) { return; } List scripts = GetScriptsInFolder(folderPath); ConvertScripts(scripts); EditorUtility.DisplayDialog("Script Conversion", "Scripts are now converted to PUN.\n\nYou will need to update\n- scenes\n- components\n- prefabs and\n- add \"PhotonNetwork.ConnectWithDefaultSettings();\"", "Ok"); } public static List GetScriptsInFolder(string folder) { List scripts = new List(); try { scripts.AddRange(Directory.GetFiles(folder, "*.cs", SearchOption.AllDirectories)); scripts.AddRange(Directory.GetFiles(folder, "*.js", SearchOption.AllDirectories)); scripts.AddRange(Directory.GetFiles(folder, "*.boo", SearchOption.AllDirectories)); } catch (System.Exception ex) { Debug.Log("Getting script list from folder " + folder + " failed. Exception:\n" + ex.ToString()); } return scripts; } static void ConvertScripts(List scriptPathList) { bool ignoreWarningIsLogged = false; foreach (string script in scriptPathList) { if (script.Contains("PhotonNetwork")) //Don't convert this file (and others) { if (!ignoreWarningIsLogged) { ignoreWarningIsLogged = true; Debug.LogWarning("Conversion to PUN ignores all files with \"PhotonNetwork\" in their file-path.\nCheck: " + script); } continue; } if (script.Contains("Image Effects")) { continue; } ConvertToPhotonAPI(script); } foreach (string script in scriptPathList) { AssetDatabase.ImportAsset(script, ImportAssetOptions.ForceUpdate); } } static void ConvertToPhotonAPI(string file) { string text = File.ReadAllText(file); bool isJS = file.Contains(".js"); file = file.Replace("\\", "/"); // Get Class name for JS string className = file.Substring(file.LastIndexOf("/")+1); className = className.Substring(0, className.IndexOf(".")); //REGEXP STUFF //Valid are: Space { } , /n /r //string NOT_VAR = @"([^A-Za-z0-9_\[\]\.]+)"; string NOT_VAR_WITH_DOT = @"([^A-Za-z0-9_]+)"; //string VAR_NONARRAY = @"[^A-Za-z0-9_]"; //NetworkView { text = PregReplace(text, NOT_VAR_WITH_DOT + "NetworkView" + NOT_VAR_WITH_DOT, "$1PhotonView$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "networkView" + NOT_VAR_WITH_DOT, "$1photonView$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "stateSynchronization" + NOT_VAR_WITH_DOT, "$1synchronization$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "NetworkStateSynchronization" + NOT_VAR_WITH_DOT, "$1ViewSynchronization$2"); // map Unity enum to ours //.RPC text = PregReplace(text, NOT_VAR_WITH_DOT + "RPCMode.Server" + NOT_VAR_WITH_DOT, "$1PhotonTargets.MasterClient$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "RPCMode" + NOT_VAR_WITH_DOT, "$1PhotonTargets$2"); } //NetworkMessageInfo: 100% { text = PregReplace(text, NOT_VAR_WITH_DOT + "NetworkMessageInfo" + NOT_VAR_WITH_DOT, "$1PhotonMessageInfo$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "networkView" + NOT_VAR_WITH_DOT, "$1photonView$2"); } //NetworkViewID: { text = PregReplace(text, NOT_VAR_WITH_DOT + "NetworkViewID" + NOT_VAR_WITH_DOT, "$1int$2"); //We simply use an int } //NetworkPlayer { text = PregReplace(text, NOT_VAR_WITH_DOT + "NetworkPlayer" + NOT_VAR_WITH_DOT, "$1PhotonPlayer$2"); } //Network { //Monobehaviour callbacks { text = PregReplace(text, NOT_VAR_WITH_DOT + "OnPlayerConnected" + NOT_VAR_WITH_DOT, "$1OnPhotonPlayerConnected$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "OnPlayerDisconnected" + NOT_VAR_WITH_DOT, "$1OnPhotonPlayerDisconnected$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "OnNetworkInstantiate" + NOT_VAR_WITH_DOT, "$1OnPhotonInstantiate$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "OnSerializeNetworkView" + NOT_VAR_WITH_DOT, "$1OnPhotonSerializeView$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "BitStream" + NOT_VAR_WITH_DOT, "$1PhotonStream$2"); //Not completely the same meaning text = PregReplace(text, NOT_VAR_WITH_DOT + "OnServerInitialized" + NOT_VAR_WITH_DOT, "$1OnCreatedRoom$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "OnConnectedToServer" + NOT_VAR_WITH_DOT, "$1OnJoinedRoom$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "OnFailedToConnectToMasterServer" + NOT_VAR_WITH_DOT, "$1OnFailedToConnectToPhoton$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "OnFailedToConnect" + NOT_VAR_WITH_DOT, "$1OnFailedToConnect_OBSELETE$2"); } //Variables { text = PregReplace(text, NOT_VAR_WITH_DOT + "Network.connections" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.playerList$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "Network.isServer" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.isMasterClient$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "Network.isClient" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.isNonMasterClientInRoom$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "NetworkPeerType" + NOT_VAR_WITH_DOT, "$1ConnectionState$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "Network.peerType" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.connectionState$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "ConnectionState.Server" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.isMasterClient$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "ConnectionState.Client" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.isNonMasterClientInRoom$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "PhotonNetwork.playerList.Length" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.playerList.Count$2"); /*DROPPED: minimumAllocatableViewIDs natFacilitatorIP is dropped natFacilitatorPort is dropped connectionTesterIP connectionTesterPort proxyIP proxyPort useProxy proxyPassword */ } //Methods { text = PregReplace(text, NOT_VAR_WITH_DOT + "Network.InitializeServer" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.CreateRoom$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "Network.Connect" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.JoinRoom$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "Network.GetAveragePing" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.GetPing$2"); text = PregReplace(text, NOT_VAR_WITH_DOT + "Network.GetLastPing" + NOT_VAR_WITH_DOT, "$1PhotonNetwork.GetPing$2"); /*DROPPED: TestConnection TestConnectionNAT HavePublicAddress */ } //Overall text = PregReplace(text, NOT_VAR_WITH_DOT + "Network" + NOT_VAR_WITH_DOT, "$1PhotonNetwork$2"); //Changed methods string ignoreMe = @"([A-Za-z0-9_\[\]\(\) ]+)"; text = PregReplace(text, NOT_VAR_WITH_DOT + "PhotonNetwork.GetPing\\(" + ignoreMe+"\\);", "$1PhotonNetwork.GetPing();"); text = PregReplace(text, NOT_VAR_WITH_DOT + "PhotonNetwork.CloseConnection\\(" + ignoreMe+","+ignoreMe+"\\);", "$1PhotonNetwork.CloseConnection($2);"); } //General { if (text.Contains("Photon")) //Only use the PhotonMonoBehaviour if we use photonView and friends. { if (isJS)//JS { if (text.Contains("extends MonoBehaviour")) text = PregReplace(text, "extends MonoBehaviour", "extends Photon.MonoBehaviour"); else text = "class " + className + " extends Photon.MonoBehaviour {\n" + text + "\n}"; } else //C# text = PregReplace(text, ": MonoBehaviour", ": Photon.MonoBehaviour"); } } File.WriteAllText(file, text); } /// default path: "Assets" public static void ConvertRpcAttribute(string path) { if (string.IsNullOrEmpty(path)) { path = "Assets"; } List scripts = GetScriptsInFolder(path); foreach (string file in scripts) { string text = File.ReadAllText(file); string textCopy = text; if (file.EndsWith("PhotonConverter.cs")) { continue; } text = text.Replace("[RPC]", "[PunRPC]"); text = text.Replace("@RPC", "@PunRPC"); if (!text.Equals(textCopy)) { File.WriteAllText(file, text); Debug.Log("Converted RPC to PunRPC in: " + file); } } } static string PregReplace(string input, string[] pattern, string[] replacements) { if (replacements.Length != pattern.Length) Debug.LogError("Replacement and Pattern Arrays must be balanced"); for (var i = 0; i < pattern.Length; i++) { input = Regex.Replace(input, pattern[i], replacements[i]); } return input; } static string PregReplace(string input, string pattern, string replacement) { return Regex.Replace(input, pattern, replacement); } static void EnsureFolder(string path) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); AssetDatabase.Refresh(); } } static int ConvertNetworkView(NetworkView[] netViews, bool isScene) { for (int i = netViews.Length - 1; i >= 0; i--) { NetworkView netView = netViews[i]; PhotonView view = netView.gameObject.AddComponent(); Undo.RecordObject(view, null); if (isScene) { //Get scene ID string str = netView.viewID.ToString().Replace("SceneID: ", ""); int firstSpace = str.IndexOf(" "); str = str.Substring(0, firstSpace); int oldViewID = int.Parse(str); view.viewID = oldViewID; #if !UNITY_MIN_5_3 EditorUtility.SetDirty(view); EditorUtility.SetDirty(view.gameObject); #endif } view.ObservedComponents = new List(); view.ObservedComponents.Add(netView.observed); if (netView.stateSynchronization == NetworkStateSynchronization.Unreliable) { view.synchronization = ViewSynchronization.Unreliable; } else if (netView.stateSynchronization == NetworkStateSynchronization.ReliableDeltaCompressed) { view.synchronization = ViewSynchronization.ReliableDeltaCompressed; } else { view.synchronization = ViewSynchronization.Off; } DestroyImmediate(netView, true); } AssetDatabase.Refresh(); AssetDatabase.SaveAssets(); return netViews.Length; } static void Output(string str) { Debug.Log(((int)EditorApplication.timeSinceStartup) + " " + str); } static void ConversionError(string file, string str) { Debug.LogError("Scrip conversion[" + file + "]: " + str); } }