using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using UnityLauncherPro.Data;
using UnityLauncherPro.Helpers;
using UnityLauncherPro.Properties;
namespace UnityLauncherPro
{
public static class Tools
{
const int SW_RESTORE = 9;
static readonly string initFileDefaultURL = "https://raw.githubusercontent.com/unitycoder/UnityInitializeProject/main/Assets/Editor/InitializeProject.cs";
[DllImport("user32", CharSet = CharSet.Unicode)]
static extern IntPtr FindWindow(string cls, string win);
[DllImport("user32")]
static extern IntPtr SetForegroundWindow(IntPtr hWnd);
[DllImport("user32")]
static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32")]
static extern bool OpenIcon(IntPtr hWnd);
[DllImport("user32")]
private static extern bool ShowWindow(IntPtr handle, int nCmdShow);
// reference to already running webgl server processes and ports
static Dictionary webglServerProcesses = new Dictionary();
// returns last modified date for file (or null if cannot get it)
public static DateTime? GetLastModifiedTime(string path)
{
if (File.Exists(path) == true || Directory.Exists(path) == true)
{
DateTime modification = File.GetLastWriteTime(path);
return modification;
}
else
{
return null;
}
}
///
/// parse project version from ProjectSettings/ data
///
/// project base path
///
public static string GetProjectVersion(string path)
{
string version = null;
if (File.Exists(Path.Combine(path, "ProjectVersionOverride.txt")))
{
version = File.ReadAllText(Path.Combine(path, "ProjectVersionOverride.txt"));
}
else if (Directory.Exists(Path.Combine(path, "ProjectSettings")))
{
var versionPath = Path.Combine(path, "ProjectSettings", "ProjectVersion.txt");
if (File.Exists(versionPath) == true) // 5.x and later
{
var data = File.ReadAllLines(versionPath);
if (data != null && data.Length > 0)
{
var dd = data[0];
// check string
if (dd.Contains("m_EditorVersion"))
{
var t = dd.Split(new string[] { "m_EditorVersion: " }, StringSplitOptions.None);
if (t != null && t.Length > 0)
{
version = t[1].Trim();
}
else
{
throw new InvalidDataException("invalid version data:" + data);
}
}
else
{
Console.WriteLine("Cannot find m_EditorVersion in '" + versionPath + "'.\n\nFile Content:\n" + string.Join("\n", data).ToString());
}
}
else
{
Console.WriteLine("Invalid projectversion data found in '" + versionPath + "'.\n\nFile Content:\n" + string.Join("\n", data).ToString());
}
}
else // maybe its 4.x?
{
versionPath = Path.Combine(path, "ProjectSettings", "ProjectSettings.asset");
if (File.Exists(versionPath) == true)
{
// first try if its ascii format
var data = File.ReadAllLines(versionPath);
if (data != null && data.Length > 0 && data[0].IndexOf("YAML") > -1) // we have ascii
{
// check library if available
var newVersionPath = Path.Combine(path, "Library", "AnnotationManager");
if (File.Exists(newVersionPath) == true)
{
versionPath = newVersionPath;
}
}
// try to get version data out from binary asset
var binData = File.ReadAllBytes(versionPath);
if (binData != null && binData.Length > 0)
{
int dataLen = 7;
int startIndex = 20;
var bytes = new byte[dataLen];
for (int i = 0; i < dataLen; i++)
{
bytes[i] = binData[startIndex + i];
}
var vertemp = Encoding.UTF8.GetString(bytes);
// probably failed if no dots
if (vertemp.IndexOf(".") > -1) version = vertemp;
}
// if still nothing, TODO probably could find closer version info, if know what features were added to playersettings.assets and checking serializedVersion: .. number
}
}
}
return version;
}
internal static string ReadProjectName(string projectPath)
{
string results = null;
var versionPath = Path.Combine(projectPath, "ProjectSettings", "ProjectSettings.asset");
if (File.Exists(versionPath) == true) // 5.x and later
{
var data = File.ReadAllLines(versionPath);
if (data != null && data.Length > 0)
{
for (int i = 0; i < data.Length; i++)
{
// check row
if (data[i].IndexOf("productName: ") > -1)
{
var t = data[i].Split(new string[] { "productName: " }, StringSplitOptions.None);
if (t != null && t.Length > 0)
{
results = t[1].Trim();
break;
}
else
{
throw new InvalidDataException("invalid productName data:" + data);
}
}
}
}
}
return results;
}
// returns unity version number string from file
public static string GetFileVersionData(string path)
{
FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(path);
var ver = fvi.ProductName;
if (string.IsNullOrEmpty(ver) == true)
{
ver = fvi.FileDescription;
if (string.IsNullOrEmpty(ver) == true) return null;
ver = ver.Replace("Installer", "").Trim();
}
var res = ver.Replace("(64-bit)", "").Replace("(32-bit)", "").Replace("Unity", "").Trim();
return res;
}
public static void ExploreFolder(string path)
{
if (path != null)
{
if (LaunchExplorer(path) == false)
{
//SetStatus("Error> Directory not found: " + folder);
}
}
}
// this runs before unity editor starts, so the project is not yet in registry (unless it already was there)
public static void AddProjectToHistory(string projectPath)
{
// fix backslashes
projectPath = projectPath.Replace('\\', '/');
if (Properties.Settings.Default.projectPaths.Contains(projectPath) == false)
{
// TODO do we need to add as first?
Properties.Settings.Default.projectPaths.Insert(0, projectPath);
// remove last item, if too many
if (Properties.Settings.Default.projectPaths.Count > MainWindow.maxProjectCount)
{
Properties.Settings.Default.projectPaths.RemoveAt(Properties.Settings.Default.projectPaths.Count - 1);
}
//Console.WriteLine("AddProjectToHistory, count: " + Properties.Settings.Default.projectPaths.Count);
// TODO no need to save everytime?
Properties.Settings.Default.Save();
// TODO need to add into recent grid also? if old items disappear?
}
}
// NOTE holding alt key (when using alt+o) brings up unity project selector
public static Process LaunchProject(Project proj, DataGrid dataGridRef = null, bool useInitScript = false, bool upgrade = false, bool cloneFromTemplate = false)
{
if (proj == null) return null;
Console.WriteLine("Launching project " + proj?.Title + " at " + proj?.Path);
if (Directory.Exists(proj.Path) == false) return null;
// add this project to recent projects in preferences TODO only if enabled +40 projecs
AddProjectToHistory(proj.Path);
// check if this project path has unity already running? (from process)
// NOTE this check only works if previous unity instance was started while we were running
if (ProcessHandler.IsRunning(proj.Path))
{
Console.WriteLine("Project is already running, lets not launch unity.. because it opens Hub");
BringProcessToFront(ProcessHandler.Get(proj.Path));
return null;
}
else
{
// TODO check lock file?
}
// there is no assets path, probably we want to create new project then
var assetsFolder = Path.Combine(proj.Path, "Assets");
if (Directory.Exists(assetsFolder) == false)
{
// TODO could ask if want to create project..?
Directory.CreateDirectory(assetsFolder);
}
// if its upgrade, we dont want to check current version
if (upgrade == false)
{
// check if project version has changed? (list is not updated, for example pulled new version from git)
var version = GetProjectVersion(proj.Path);
if (string.IsNullOrEmpty(version) == false && version != proj.Version)
{
Console.WriteLine("Project version has changed from " + proj.Version + " to " + version);
proj.Version = version;
}
}
// check if we have this unity version installed
var unityExePath = GetUnityExePath(proj.Version);
if (unityExePath == null)
{
// if no editors installed, show message
if (MainWindow.unityInstallationsSource.Count == 0)
{
MessageBox.Show($"No Unity versions installed. Please run {MainWindow.appName} first to setup root folders.", MainWindow.appName, MessageBoxButton.OK, MessageBoxImage.Warning);
return null;
}
DisplayUpgradeDialog(proj, null, useInitScript);
return null;
}
// when opening project, check for crashed backup scene first
var cancelLaunch = CheckCrashBackupScene(proj.Path);
if (cancelLaunch == true)
{
return null;
}
Process newProcess = new Process();
try
{
var cmd = "\"" + unityExePath + "\"";
newProcess.StartInfo.FileName = cmd;
string unitycommandlineparameters = (cloneFromTemplate ? " -createproject " : " -projectPath ") + "\"" + proj.Path + "\"";
string customArguments = proj.Arguments;
if (string.IsNullOrEmpty(customArguments) == false)
{
unitycommandlineparameters += " " + customArguments;
}
string projTargetPlatform = proj.TargetPlatform;
if (string.IsNullOrEmpty(projTargetPlatform) == false)
{
unitycommandlineparameters += " -buildTarget " + projTargetPlatform;
}
if (useInitScript == true)
{
unitycommandlineparameters += " -executeMethod UnityLauncherProTools.InitializeProject.Init";
}
Console.WriteLine("Start process: " + cmd + " " + unitycommandlineparameters);
// TODO load custom settings per project
//string userSettingsFolder = Path.Combine(proj.Path, "UserSettings");
//string userSettingsPath = Path.Combine(userSettingsFolder, "ULPSettings.txt");
//if (File.Exists(userSettingsPath))
//{
// var rawSettings = File.ReadAllLines(userSettingsPath);
// // needed for env vars.
// newProcess.StartInfo.UseShellExecute = false;
// foreach (var row in rawSettings)
// {
// var split = row.Split('=');
// if (split.Length == 2)
// {
// var key = split[0].Trim();
// var value = split[1].Trim();
// if (string.IsNullOrEmpty(key) == false && string.IsNullOrEmpty(value) == false)
// {
// //Console.WriteLine("key: " + key + " value: " + value);
// //newProcess.StartInfo.EnvironmentVariables[key] = value;
// //System.Environment.SetEnvironmentVariable(key, value, EnvironmentVariableTarget.Machine);
// var dict = newProcess.StartInfo.EnvironmentVariables;
// // print all
// foreach (System.Collections.DictionaryEntry de in dict)
// {
// Console.WriteLine(" {0} = {1}", de.Key, de.Value);
// }
// // check if key exists
// if (dict.ContainsKey(key) == true)
// {
// // modify existing
// //dict[key] = value;
// newProcess.StartInfo.EnvironmentVariables.Remove(key);
// newProcess.StartInfo.EnvironmentVariables.Add(key, value);
// }
// else
// {
// // add new
// dict.Add(key, value);
// }
// //newProcess.StartInfo.EnvironmentVariables.
// //if (newProcess.StartInfo.EnvironmentVariables.ContainsKey(key))
// //{
// // Console.WriteLine("exists: "+key);
// // // Test Modify the existing environment variable
// // newProcess.StartInfo.EnvironmentVariables[key] = "...";
// // this works, maybe because its not a system variable?
// //newProcess.StartInfo.EnvironmentVariables["TESTTEST"] = "...";
// //}
// //else
// //{
// // Console.WriteLine("add new: "+ value);
// // // Optionally, add the environment variable if it does not exist
// // newProcess.StartInfo.EnvironmentVariables.Add(key, value);
// //}
// Console.WriteLine("custom row: " + row + " key=" + key + " value:" + value);
// }
// }
// }
//}
newProcess.StartInfo.Arguments = unitycommandlineparameters;
newProcess.EnableRaisingEvents = true;
//newProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; // needed for unity 2023 for some reason? (otherwise console popups briefly), Cannot use this, whole Editor is invisible then
newProcess.Start();
if (Properties.Settings.Default.closeAfterProject)
{
Environment.Exit(0);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
// NOTE move project as first, since its opened, disabled for now, since its too jumpy..
//MainWindow wnd = (MainWindow)Application.Current.MainWindow;
//wnd.MoveRecentGridItem(0);
ProcessHandler.Add(proj, newProcess);
return newProcess;
}
static bool CheckCrashBackupScene(string projectPath)
{
var cancelRunningUnity = false;
var recoveryFile = Path.Combine(projectPath, "Temp", "__Backupscenes", "0.backup");
if (File.Exists(recoveryFile))
{
if (Settings.Default.showRecoveredScenePopup)
{
var result = MessageBox.Show("Crash recovery scene found, do you want to MOVE it into Assets/_Recovery/-folder?", "UnityLauncherPro - Scene Recovery", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
var restoreFolder = Path.Combine(projectPath, "Assets", "_Recovery");
if (Directory.Exists(restoreFolder) == false)
{
Directory.CreateDirectory(restoreFolder);
}
if (Directory.Exists(restoreFolder) == true)
{
Int32 unixTimestamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
var uniqueFileName = "Recovered_Scene" + unixTimestamp + ".unity";
try
{
File.Move(recoveryFile, Path.Combine(restoreFolder, uniqueFileName));
// remove folder, otherwise unity 6000.2 asks for recovery
Directory.Delete(Path.Combine(projectPath, "Temp", "__Backupscenes"), true);
Console.WriteLine("moved file to " + uniqueFileName);
}
catch (IOException)
{
// if move failed, try copy
File.Copy(recoveryFile, Path.Combine(restoreFolder, uniqueFileName));
Console.WriteLine("copied file");
}
Console.WriteLine("Recovered crashed scene into: " + restoreFolder);
}
else
{
Console.WriteLine("Error: Failed to create restore folder: " + restoreFolder);
cancelRunningUnity = true;
}
}
else if (result == MessageBoxResult.Cancel) // dont do restore, but run Unity
{
cancelRunningUnity = true;
}
}
else
{
File.Delete(recoveryFile);
Directory.Delete(Path.GetDirectoryName(recoveryFile));
}
}
return cancelRunningUnity;
}
public static string GetUnityExePath(string version)
{
if (string.IsNullOrEmpty(version) == true) return null;
return MainWindow.unityInstalledVersions.ContainsKey(version) ? MainWindow.unityInstalledVersions[version] : null;
}
// opens Explorer to target folder
public static bool LaunchExplorer(string folder)
{
if (folder == null || folder.Length < 1) return false;
if (Directory.Exists(folder) == true)
{
Process.Start(folder);
return true;
}
else // original folder is missing, try to find parent folder that we can go into
{
for (int i = folder.Length - 1; i > -1; i--)
{
// TODO path.separator
if (folder[i] == '/')
{
if (Directory.Exists(folder.Substring(0, i)))
{
Process.Start(folder.Substring(0, i) + "/");
break;
}
}
}
}
return false;
}
public static bool LaunchExplorerSelectFile(string fileName)
{
if (File.Exists(fileName) == true)
{
fileName = Path.GetFullPath(fileName);
Process.Start("explorer.exe", string.Format("/select,\"{0}\"", fileName));
return true;
}
else // file is missing, try to find parent folder that we can go into
{
for (int i = fileName.Length - 1; i > -1; i--)
{
if (fileName[i] == '/')
{
if (Directory.Exists(fileName.Substring(0, i)))
{
Process.Start(fileName.Substring(0, i) + "/");
break;
}
}
}
}
return false;
}
// run any exe, return process
public static Process LaunchExe(string path, string param = null, bool captureOutput = false)
{
if (string.IsNullOrEmpty(path)) return null;
// not needed for exe's in PATH
//if (File.Exists(path) == true)
{
Process newProcess = null;
if (string.IsNullOrEmpty(param) == true)
{
Console.WriteLine("LaunchExe= " + path);
newProcess = Process.Start(path);
}
else
{
Console.WriteLine("LaunchExe= " + path + " param=" + param);
try
{
newProcess = new Process();
newProcess.StartInfo.FileName = "\"" + path + "\"";
newProcess.StartInfo.Arguments = param;
if (captureOutput)
{
newProcess.StartInfo.RedirectStandardError = true;
newProcess.StartInfo.RedirectStandardOutput = true;
newProcess.StartInfo.UseShellExecute = false;
}
newProcess.EnableRaisingEvents = true; // needed to get Exited event
newProcess.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
return newProcess;
}
// Console.WriteLine("Failed to run exe: " + path + " " + param);
// return null;
}
public static string GetUnityReleaseURL(string version)
{
if (string.IsNullOrEmpty(version)) return null;
var cleanVersion = CleanVersionNumber(version);
string url = $"https://unity.com/releases/editor/whats-new/{cleanVersion}#notes";
//if (VersionIsArchived(version) == true)
//{
// // remove f#, TODO should remove c# from china version ?
// version = Regex.Replace(version, @"f[0-9]{1,2}", "", RegexOptions.IgnoreCase);
// string padding = "unity-";
// string whatsnew = "whats-new";
// if (version.Contains("5.6")) padding = "";
// if (version.Contains("2018.2")) whatsnew = "whatsnew";
// if (version.Contains("2018.3")) padding = "";
// if (version.Contains("2018.1")) whatsnew = "whatsnew";
// if (version.Contains("2017.4.")) padding = "";
// if (version.Contains("2018.4.")) padding = "";
// // later versions seem to follow this
// var year = int.Parse(version.Split('.')[0]);
// if (year >= 2019) padding = "";
// url = "https://unity3d.com/unity/" + whatsnew + "/" + padding + version;
//}
//else
//if (VersionIsPatch(version) == true)
//{
// url = "https://unity3d.com/unity/qa/patch-releases/" + version;
//}
//else
//if (VersionIsBeta(version) == true)
//{
// url = "https://unity3d.com/unity/beta/" + version;
//}
//else
//if (VersionIsAlpha(version) == true)
//{
// url = "https://unity3d.com/unity/alpha/" + version;
//}
return url;
}
// if version contains *f* its archived version
public static bool VersionIsArchived(string version)
{
return version.Contains("f");
}
public static bool VersionIsPatch(string version)
{
return version.Contains("p");
}
public static bool VersionIsBeta(string version)
{
return version.Contains("b");
}
public static bool VersionIsAlpha(string version)
{
return version.Contains("a");
}
public static bool VersionIsChinese(string version)
{
return version.Contains("c1");
}
//as of 21 May 2021, only final 'f' versions are now available on the alpha release notes for Unity 2018 and newer. 2017 and 5 still have patch 'p' versions as well.
public static bool HasAlphaReleaseNotes(string version) => VersionIsArchived(version) || VersionIsPatch(version);
public static string GetAlphaReleaseNotesURL(string fromVersion, string toVersion = null)
=> "https://alpha.release-notes.ds.unity3d.com/search?fromVersion=" + fromVersion + "&toVersion=" + (toVersion != null ? toVersion : fromVersion);
// open release notes page in browser
public static bool OpenReleaseNotes(string version)
{
bool result = false;
if (string.IsNullOrEmpty(version)) return false;
string url = null;
if (Properties.Settings.Default.useAlphaReleaseNotes && HasAlphaReleaseNotes(version))
{
url = GetAlphaReleaseNotesURL(version);
}
else
{
url = GetUnityReleaseURL(version);
}
if (string.IsNullOrEmpty(url)) return false;
OpenURL(url);
result = true;
return result;
}
public static bool OpenReleaseNotes_Cumulative(string version)
{
bool result = false;
if (string.IsNullOrEmpty(version)) return false;
string url = null;
var comparisonVersion = version;
//with the alpha release notes, we want a diff between an installed version and the one selected, but the site just shows all the changes inclusive of "fromVersion=vers"
//so if we find a good installed candidate, we need the version just above it (installed or not) that has release notes page
var closestInstalledVersion = Tools.FindNearestVersion(version, MainWindow.unityInstalledVersions.Keys.ToList(), true);
if (closestInstalledVersion != null)
{
comparisonVersion = closestInstalledVersion;
string nextFinalVersionAfterInstalled = closestInstalledVersion;
//wwe need a loop here, to find the nearest final version. It might be better to warn the user about this before opening the page.
do
nextFinalVersionAfterInstalled = Tools.FindNearestVersion(nextFinalVersionAfterInstalled, MainWindow.updatesAsStrings);
while (nextFinalVersionAfterInstalled != null && !HasAlphaReleaseNotes(nextFinalVersionAfterInstalled));
if (nextFinalVersionAfterInstalled != null) comparisonVersion = nextFinalVersionAfterInstalled;
}
url = GetAlphaReleaseNotesURL(comparisonVersion, version);
OpenURL(url);
result = true;
return result;
}
public static void OpenURL(string url)
{
Process.Start(url);
}
public static async void DownloadInBrowser(string version, bool preferFullInstaller = false)
{
if (version == null) return;
string exeURL = await GetUnityUpdates.FetchDownloadUrl(version);
// null from unity api? then try direct download
// https://beta.unity3d.com/download/330fbefc18b7/UnityDownloadAssistant-6000.1.0a8.exe
if (exeURL == null)
{
// exe url not found, try unofficial list (TODO this is hack to avoid null url, because unofficial list items have been cached to local json, without download url..)
Console.WriteLine("Fixing null in DownloadInBrowser ,v=" + version);
var downloadURL = await GetUnityUpdates.CheckUnofficialVersionList(version);
if (string.IsNullOrEmpty(downloadURL) == false)
{
string unityHash = ParseHashCodeFromURL(downloadURL);
exeURL = ParseDownloadURLFromWebpage(version, unityHash, false, true);
}
return;
}
if (preferFullInstaller == true)
{
exeURL = exeURL.Replace("UnityDownloadAssistant-" + version + ".exe", "Windows64EditorInstaller/UnitySetup64-" + version + ".exe");
}
Console.WriteLine("DownloadInBrowser exeURL= '" + exeURL + "'");
if (string.IsNullOrEmpty(exeURL) == false && exeURL.StartsWith("https"))
{
//SetStatus("Download installer in browser: " + exeURL);
Process.Start(exeURL);
}
else // not found
{
//SetStatus("Error> Cannot find installer executable ... opening website instead");
const string url = "https://unity3d.com/get-unity/download/archive";
Process.Start(url + "#installer-not-found-for-version-" + version);
}
}
public static async void DownloadAndInstall(string version)
{
string exeURL = await GetUnityUpdates.FetchDownloadUrl(version);
if (version == null)
{
Console.WriteLine("Error> Cannot download and install null version");
return;
}
if (string.IsNullOrEmpty(exeURL) == true)
{
// exe url not found, try unofficial list (TODO this is hack to avoid null url, because unofficial list items have been cached to local json, without download url..)
Console.WriteLine("Fixing null in DownloadInBrowser ,v=" + version);
var downloadURL = await GetUnityUpdates.CheckUnofficialVersionList(version);
if (string.IsNullOrEmpty(downloadURL) == false)
{
string unityHash = ParseHashCodeFromURL(downloadURL);
exeURL = ParseDownloadURLFromWebpage(version, unityHash, false, true);
}
}
Console.WriteLine("download exeURL= (" + exeURL + ")");
if (string.IsNullOrEmpty(exeURL) == false && exeURL.StartsWith("https") == true)
{
//SetStatus("Download installer in browser: " + exeURL);
// download url file to temp
string tempFile = Path.GetTempPath() + "UnityDownloadAssistant-" + version.Replace(".", "_") + ".exe";
//Console.WriteLine("download tempFile= (" + tempFile + ")");
if (File.Exists(tempFile) == true) File.Delete(tempFile);
// TODO make async
if (await DownloadFileAsync(exeURL, tempFile))
{
// get base version, to use for install path
// FIXME check if have any paths?
string lastRootFolder = Properties.Settings.Default.rootFolders[Properties.Settings.Default.rootFolders.Count - 1];
// check if ends with / or \
if (lastRootFolder.EndsWith("/") == false && lastRootFolder.EndsWith("\\") == false) lastRootFolder += "\\";
string outputVersionFolder = version.Split('.')[0] + "_" + version.Split('.')[1];
string targetPathArgs = " /D=" + lastRootFolder + outputVersionFolder; ;
// NOTE cannot enable same modules as the previously installed version, because it requires elevated permissions..
// GetPlatformsForUnityVersion..
//var previousVersion = Tools.FindNearestVersion(version, MainWindow.unityInstalledVersions.Keys.ToList(), true);
//if (previousVersion != null)
//{
// string[] previousModules = GetPlatformsForUnityVersion(previousVersion);
// if (previousModules != null && previousModules.Length > 0)
// {
// // find registry Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Unity Technologies\Installer
// // then enable dword 1, for example key: "Select_Android", if my modules has "Android" etc.
// foreach (var module in previousModules)
// {
// Console.WriteLine(previousVersion + " has modules: " + module);
// // 6000.0.66f2 has modules: Android
// // 6000.0.66f2 has modules: iOS
// // 6000.0.66f2 has modules: Win
// // 6000.0.66f2 has modules: Win64
// }
// }
//}
// if user clicks NO to UAC, this fails (so added try-catch)
try
{
Process process = new Process();
process.StartInfo.FileName = tempFile;
process.StartInfo.Arguments = targetPathArgs;
process.EnableRaisingEvents = true;
process.Exited += (sender, e) => InstallationCompleted(tempFile);
process.Start();
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223) // ERROR_CANCELLED
{
// User declined the UAC prompt
Console.WriteLine("User cancelled elevation (UAC).");
SetStatus("Installation cancelled by user.");
InstallationCompleted(tempFile);
}
catch (Exception ex)
{
Console.WriteLine("Failed to run exe: " + tempFile + " - " + ex.Message);
SetStatus("Failed to run installer: " + ex.Message);
InstallationCompleted(tempFile);
}
// TODO refresh upgrade dialog after installer finished
}
}
else // not found
{
//SetStatus("Error> Cannot find installer executable ... opening website instead");
var url = "https://unity3d.com/get-unity/download/archive";
Process.Start(url + "#installer-not-found---version-" + version);
SetStatus("Error> Cannot find installer executable for version " + version);
}
} // DownloadAndInstall
public static async Task DownloadInitScript(string currentInitScriptFullPath, string currentInitScriptLocationOrURL)
{
string currentInitScriptFolder = Path.GetDirectoryName(currentInitScriptFullPath);
string currentInitScriptFile = Path.GetFileName(currentInitScriptFullPath);
string tempFile = Path.Combine(Path.GetTempPath(), currentInitScriptFile);
bool isLocalFile = false;
if (string.IsNullOrEmpty(currentInitScriptLocationOrURL) == true) currentInitScriptLocationOrURL = initFileDefaultURL;
// check if its URL or local file
if (currentInitScriptLocationOrURL.ToLower().StartsWith("http") == true)
{
// download into temp first
if (await DownloadFileAsync(currentInitScriptLocationOrURL, tempFile) == false) return;
}
else // file is in local folders/drives/projects
{
// check if file exists
if (File.Exists(currentInitScriptLocationOrURL) == false) return;
tempFile = currentInitScriptLocationOrURL;
isLocalFile = true;
}
// if got file
if (File.Exists(tempFile) == true)
{
// just in case file is locked
try
{
// small validation to check if its valid editor script
var tempContent = File.ReadAllText(tempFile);
if (tempContent.IndexOf("public class InitializeProject") > 0 && tempContent.IndexOf("namespace UnityLauncherProTools") > 0 && tempContent.IndexOf("public static void Init()") > 0)
{
// create scripts folder if missing
if (Directory.Exists(currentInitScriptFolder) == false) Directory.CreateDirectory(currentInitScriptFolder);
// move old file as backup
if (File.Exists(currentInitScriptFullPath))
{
string oldScriptFullPath = Path.Combine(currentInitScriptFolder, currentInitScriptFile + ".bak");
if (File.Exists(oldScriptFullPath)) File.Delete(oldScriptFullPath);
File.Move(currentInitScriptFullPath, oldScriptFullPath);
}
// move new file here (need to delete old to overwrite)
if (File.Exists(currentInitScriptFullPath)) File.Delete(currentInitScriptFullPath);
// local file copy, not move
if (isLocalFile == true)
{
File.Copy(tempFile, currentInitScriptFullPath);
}
else
{
File.Move(tempFile, currentInitScriptFullPath);
}
SetStatus("Downloaded latest init script.");
}
else
{
Console.WriteLine("Invalid c# init file..(missing correct Namespace, Class or Method)");
SetStatus("Invalid c# init file..(missing correct Namespace, Class or Method)");
}
}
catch (Exception e)
{
Console.WriteLine("File exception: " + e.Message);
SetStatus("File exception: " + e.Message);
}
}
else
{
Console.WriteLine("Failed to download init script from: " + currentInitScriptLocationOrURL);
SetStatus("Failed to download init script from: " + currentInitScriptLocationOrURL);
}
}
static void InstallationCompleted(string path)
{
if (File.Exists(path) == true)
{
// delete temp installer file
File.Delete(path);
// refresh installed versions list
mainWindow.Dispatcher.Invoke(() =>
{
mainWindow.UpdateUnityInstallationsList();
mainWindow.CallGetUnityUpdates();
});
}
}
public static string DownloadHTML(string url)
{
Console.WriteLine("DownloadHTML: " + url);
if (string.IsNullOrEmpty(url) == true) return null;
using (WebClient client = new WebClient())
{
try
{
// download page html
return client.DownloadString(url);
}
catch (WebException e)
{
Console.WriteLine("DownloadHTML: " + e.Message);
return null;
}
}
}
public static string CleanVersionNumber(string version)
{
if (string.IsNullOrEmpty(version)) return null;
var split = version.Split('.');
float parsedVersion = float.Parse($"{split[0]}.{split[1]}");
// For 2023.3 and newer pre-release (alpha or beta) versions, do not clean.
if ((IsAlpha(version) || version.Contains("b")) && parsedVersion >= 2023.3)
{
// Do nothing; leave version unchanged.
}
else
{
// Remove the trailing patch/build indicator.
version = Regex.Replace(version, @"[fab][0-9]{1,2}", "", RegexOptions.IgnoreCase);
}
return version;
}
// TODO only hash version is used, cleanup the rest
public static string ParseDownloadURLFromWebpage(string version, string hash = null, bool preferFullInstaller = false, bool useHash = false)
{
string exeURL = "";
//Console.WriteLine("ParseDownloadURLFromWebpage: " + version + ", hash: " + useHash);
if (string.IsNullOrEmpty(version)) return null;
// NOTE no longer uses f# in the end
string url = null;
if (useHash == false)
{
var cleanVersion = CleanVersionNumber(version);
// NOTE 2024 June, installs are now located on separate pages, like https://unity.com/releases/editor/whats-new/6000.0.5#installs
// get correct page url
//url = "https://unity3d.com/get-unity/download/archive";
// fix unity server problem, some pages says 404 found if no url params
url = "https://unity.com/releases/editor/whats-new/" + cleanVersion + "?unitylauncherpro#installs";
//if (VersionIsPatch(version)) url = "https://unity3d.com/unity/qa/patch-releases";
if (VersionIsBeta(version)) url = "https://unity.com/releases/editor/beta/" + version;
if (VersionIsAlpha(version)) url = "https://unity.com/releases/editor/alpha/" + version;
//url += "?unitylauncherpro";
}
else
{
// NOTE version here is actually VERSION|HASH
//string hash = version;
url = $"https://beta.unity3d.com/download/{hash}/download.html";
//Console.WriteLine("hashurl: " + url);
//version = FetchUnityVersionNumberFromHTML(url);
//Console.WriteLine(url);
//Console.WriteLine("got "+version);
if (string.IsNullOrEmpty(version))
{
SetStatus("Failed to get version (" + version + ") number from hash: " + hash);
return null;
}
}
//Console.WriteLine("scanning installers from url: " + url);
//string sourceHTML = DownloadHTML(url);
//if (string.IsNullOrEmpty(sourceHTML) == true)
//{
// Console.WriteLine("Failed to download html from: " + url);
// return null;
//}
//// parse changeset hash from html
//string pattern = $@"href=""unityhub://{version}/([^""]+)""";
//Regex regex = new Regex(pattern);
//Match match = regex.Match(sourceHTML);
//if (match.Success == true)
//{
// string changeSet = match.Groups[1].Value;
// Console.WriteLine("changeSet: " + changeSet);
//}
exeURL = $"https://beta.unity3d.com/download/{hash}/UnityDownloadAssistant-{version}.exe";
//string[] lines = sourceHTML.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
// patch version download assistant finder
//if (useHash == false && VersionIsPatch(version))
//{
// for (int i = 0; i < lines.Length; i++)
// {
// //if (lines[i].Contains("UnityDownloadAssistant-" + version + ".exe"))
// if (lines[i].Contains("UnitySetup64-" + version + ".exe"))
// {
// int start = lines[i].IndexOf('"') + 1;
// int end = lines[i].IndexOf('"', start);
// exeURL = lines[i].Substring(start, end - start);
// break;
// }
// }
//}
//else if (useHash == false && VersionIsArchived(version))
//{
// // archived version download assistant finder
// for (int i = 0; i < lines.Length; i++)
// {
// // find line where full installer is (from archive page)
// if (lines[i].Contains("UnitySetup64-" + version))
// {
// Console.WriteLine(lines[i]);
// // take full exe installer line, to have changeset hash, then replace with download assistant filepath
// string line = lines[i];
// int start = line.IndexOf('"') + 1;
// int end = line.IndexOf('"', start);
// exeURL = line.Substring(start, end - start);
// exeURL = exeURL.Replace("Windows64EditorInstaller/UnitySetup64-", "UnityDownloadAssistant-");
// break;
// }
// }
//}
//else // alpha or beta version download assistant finder
//{
// // regular beta
// //
// // https://beta.unity3d.com/download/21aeb48b6ed2/UnityDownloadAssistant.exe
// // hidden beta
// //
// // https://beta.unity3d.com/download/8008bc0c1b74/UnityDownloadAssistant-6000.0.0b15.exe
// // new 10.06.2024, no more downloadassistant.exe in html
// // check html lines
// for (int i = 0; i < lines.Length; i++)
// {
// //Console.WriteLine(lines[i]);
// //if (lines[i].Contains("UnityDownloadAssistant"))
// if (lines[i].Contains("UnityDownloadAssistant"))
// {
// if (useHash == false)
// {
// string pattern = @"https://beta\.unity3d\.com/download/[a-zA-Z0-9]+/UnityDownloadAssistant\.exe";
// Match match = Regex.Match(lines[i], pattern);
// if (match.Success)
// {
// exeURL = match.Value;
// }
// else
// {
// Console.WriteLine("No match found for download base url..");
// }
// }
// else // hidden download page
// {
// string pattern = @"UnityDownloadAssistant(?:-\d+\.\d+\.\d+[bf]\d*)?\.exe";
// Match match = Regex.Match(lines[i], pattern);
// if (match.Success)
// {
// // append base url
// Regex regex = new Regex(@"(https://beta\.unity3d\.com/download/[a-zA-Z0-9]+/)");
// Match match2 = regex.Match(url);
// //Console.WriteLine("source url: " + url);
// if (match2.Success)
// {
// string capturedUrl = match2.Groups[1].Value;
// exeURL = capturedUrl + match.Value;
// }
// else
// {
// Console.WriteLine("No match found for download base url..");
// }
// }
// break;
// }
// }
// } // for lines
//} // alpha or beta
// download full installer instead, TODO probably not needed anymore?
if (useHash == false && preferFullInstaller == true)
{
exeURL = exeURL.Replace("UnityDownloadAssistant-" + version + ".exe", "Windows64EditorInstaller/UnitySetup64-" + version + ".exe");
// handle alpha/beta
exeURL = exeURL.Replace("UnityDownloadAssistant.exe", "Windows64EditorInstaller/UnitySetup64-" + version + ".exe");
}
// didnt find installer
if (string.IsNullOrEmpty(exeURL))
{
//SetStatus("Cannot find UnityDownloadAssistant.exe for this version.");
Console.WriteLine("Installer not found from URL: " + url);
}
return exeURL;
}
private static string FetchUnityVersionNumberFromHTML(string url)
{
string sourceHTML = DownloadHTML(url);
if (string.IsNullOrEmpty(sourceHTML)) return null;
string pattern = @"\d+\.\d+\.\d+[bf]\d+";
MatchCollection matches = Regex.Matches(sourceHTML, pattern);
if (matches.Count > 0)
{
foreach (Match match in matches)
{
Console.WriteLine("Extracted number: " + match.Value);
return match.Value;
//break;
}
}
else
{
Console.WriteLine("FetchUnityVersionNumberFromHTML: No match found.");
}
return null;
//if (string.IsNullOrEmpty(sourceHTML) == false)
//{
// // find version number from html
// string pattern = @"UnityDownloadAssistant-[0-9]+\.[0-9]+\.[0-9]+[a-z]?\.exe";
// Match match = Regex.Match(sourceHTML, pattern);
// if (match.Success)
// {
// version = match.Value.Replace("UnityDownloadAssistant-", "").Replace(".exe", "");
// }
//}
//return version;
}
public static string FindNearestVersion(string currentVersion, List allAvailable, bool checkBelow = false)
{
if (allAvailable == null)
return null;
string result = null;
// add current version to list, to sort it with others
if (!allAvailable.Contains(currentVersion)) allAvailable.Add(currentVersion);
// sort list
if (checkBelow)
{
allAvailable.Sort((s1, s2) => VersionAsLong(s1).CompareTo(VersionAsLong(s2)));
}
else
{
allAvailable.Sort((s1, s2) => VersionAsLong(s2).CompareTo(VersionAsLong(s1)));
}
// check version above our current version
int currentIndex = allAvailable.IndexOf(currentVersion);
// if its index 0, we select that row anyways later
if (currentIndex > 0 && currentIndex < allAvailable.Count)
{
result = allAvailable[currentIndex - 1];
}
return result;
}
// returns version as integer, for easier sorting between versions: 2019.4.19f1 = 2019041901
public static long VersionAsLong(string version)
{
long result = 0;
// cleanup 32bit version name, TODO is this needed anymore?
string cleanVersion = version.Replace("(32-bit)", "");
// remove a (alpha),b (beta),f (final?),p (path),c (china final)
cleanVersion = cleanVersion.Replace("a", ".1.");
cleanVersion = cleanVersion.Replace("b", ".2.");
cleanVersion = cleanVersion.Replace("c", ".3."); // NOTE this was 'c1'
cleanVersion = cleanVersion.Replace("f", ".4.");
cleanVersion = cleanVersion.Replace("p", ".5.");
// split values
string[] splitted = cleanVersion.Split('.');
if (splitted.Length > 1)
{
long multiplier = 1;
for (long i = 0, length = splitted.Length; i < length; i++)
{
long n = int.Parse(splitted[length - 1 - i]);
result += n * multiplier;
multiplier *= 50;
}
}
return result;
}
// https://stackoverflow.com/a/1619103/5452781
public static KeyValuePair GetEntry(this IDictionary dictionary, TKey key)
{
return new KeyValuePair(key, dictionary[key]);
}
public static void HandleDataGridScrollKeys(object sender, KeyEventArgs e)
{
/*
DataGrid grid = sender as DataGrid;
switch (e.Key)
{
case Key.Up:
if (grid.SelectedIndex > 0)
{
grid.SelectedIndex--;
}
// disable wrap around
//else
//{
// grid.SelectedIndex = grid.Items.Count - 1;
//}
e.Handled = true;
break;
case Key.Down:
if (grid.SelectedIndex < grid.Items.Count)
{
grid.SelectedIndex++;
}
//grid.SelectedIndex = ++grid.SelectedIndex % grid.Items.Count;
e.Handled = true;
break;
}
grid.ScrollIntoView(grid.Items[grid.SelectedIndex]);
*/
}
// NOTE this doesnt modify the 2nd line in ProjectVersion.txt
static void SaveProjectVersion(Project proj)
{
var settingsPath = Path.Combine(proj.Path, "ProjectSettings", "ProjectVersion.txt");
if (File.Exists(settingsPath))
{
var versionRows = File.ReadAllLines(settingsPath);
versionRows[0] = "m_EditorVersion: " + proj.Version;
File.WriteAllLines(settingsPath, versionRows);
}
}
public static void DisplayUpgradeDialog(Project proj, MainWindow owner, bool useInitScript = false)
{
UpgradeWindow modalWindow = new UpgradeWindow(proj.Version, proj.Path, proj.Arguments);
modalWindow.ShowInTaskbar = owner == null;
modalWindow.WindowStartupLocation = owner == null ? WindowStartupLocation.CenterScreen : WindowStartupLocation.CenterOwner;
modalWindow.Topmost = owner == null;
modalWindow.ShowActivated = true;
modalWindow.Owner = owner;
modalWindow.ShowDialog();
var results = modalWindow.DialogResult.HasValue && modalWindow.DialogResult.Value;
if (results == true)
{
var upgradeToVersion = UpgradeWindow.upgradeVersion;
if (string.IsNullOrEmpty(upgradeToVersion)) return;
// get selected version to upgrade for
Console.WriteLine("Upgrade to " + upgradeToVersion);
// inject new version for this item
proj.Version = upgradeToVersion;
SaveProjectVersion(proj);
var proc = LaunchProject(proj, dataGridRef: null, useInitScript: false, upgrade: true);
// TODO update datagrid row for new version
}
else
{
//Console.WriteLine("results = " + results);
}
}
///
/// install context menu item to registry
///
///
public static void AddContextMenuRegistry(string contextRegRoot)
{
RegistryKey key = Registry.CurrentUser.OpenSubKey(contextRegRoot, true);
// add folder if missing
if (key == null)
{
key = Registry.CurrentUser.CreateSubKey(@"Software\Classes\Directory\Background\Shell");
}
if (key != null)
{
var appName = "UnityLauncherPro";
key.CreateSubKey(appName);
key = key.OpenSubKey(appName, true);
key.SetValue("", "Open with " + appName);
key.SetValue("Icon", "\"" + Process.GetCurrentProcess().MainModule.FileName + "\"");
key.CreateSubKey("command");
key = key.OpenSubKey("command", true);
var executeString = "\"" + Process.GetCurrentProcess().MainModule.FileName + "\"";
executeString += " -projectPath \"%V\"";
key.SetValue("", executeString);
}
else
{
Console.WriteLine("Error> Cannot find registry key: " + contextRegRoot);
}
}
public static void AddContextMenuRegistryAPKInstall(string contextRegRoot)
{
// Define the registry key path for .apk file association
string apkFileTypeRegPath = @"Software\Classes\.apk";
// Open or create the registry key for .apk files
RegistryKey apkKey = Registry.CurrentUser.OpenSubKey(apkFileTypeRegPath, true);
if (apkKey == null)
{
apkKey = Registry.CurrentUser.CreateSubKey(apkFileTypeRegPath);
}
if (apkKey != null)
{
// Create or open the Shell subkey for context menu options
RegistryKey shellKey = apkKey.CreateSubKey("shell", true);
if (shellKey != null)
{
var appName = "UnityLauncherPro";
// Create a subkey for the app's context menu item
RegistryKey appKey = shellKey.CreateSubKey(appName, true);
if (appKey != null)
{
appKey.SetValue("", "Install with " + appName); // Display name in context menu
appKey.SetValue("Icon", "\"" + Process.GetCurrentProcess().MainModule.FileName + "\"");
appKey.SetValue("Position", "Bottom"); // Set position to adjust order
// Create the command subkey to specify the action
RegistryKey commandKey = appKey.CreateSubKey("command", true);
if (commandKey != null)
{
// Build the command string to launch with -install argument
var executeString = "\"" + Process.GetCurrentProcess().MainModule.FileName + "\" -install \"%1\"";
commandKey.SetValue("", executeString);
}
}
}
}
else
{
Console.WriteLine("Error> Cannot create or access registry key for .apk file association: " + apkFileTypeRegPath);
}
}
///
/// uninstall context menu item from registry
///
///
public static void RemoveContextMenuRegistry(string contextRegRoot)
{
RegistryKey key = Registry.CurrentUser.OpenSubKey(contextRegRoot, true);
if (key != null)
{
var appName = "UnityLauncherPro";
RegistryKey appKey = Registry.CurrentUser.OpenSubKey(contextRegRoot + "\\" + appName, false);
if (appKey != null)
{
key.DeleteSubKeyTree(appName);
//SetStatus("Removed context menu registry items");
}
else
{
//SetStatus("Nothing to uninstall..");
}
}
else
{
//SetStatus("Error> Cannot find registry key: " + contextRegRoot);
}
}
public static void RemoveContextMenuRegistryAPKInstall(string contextRegRoot)
{
// Define the registry key path for .apk file association
string apkFileTypeRegPath = @"Software\Classes\.apk\shell";
// Open the registry key for the shell context menu
RegistryKey shellKey = Registry.CurrentUser.OpenSubKey(apkFileTypeRegPath, true);
if (shellKey != null)
{
var appName = "UnityLauncherPro";
// Check if the app's context menu key exists
RegistryKey appKey = shellKey.OpenSubKey(appName, false);
if (appKey != null)
{
// Delete the app's context menu key
shellKey.DeleteSubKeyTree(appName);
Console.WriteLine("Removed context menu for .apk files.");
}
else
{
Console.WriteLine("No context menu found for .apk files.");
}
}
else
{
Console.WriteLine("Error> Cannot find registry key for .apk shell context: " + apkFileTypeRegPath);
}
}
///
/// reads .git/HEAD file from the project to get current branch name
///
///
///
///
public static string ReadGitBranchInfo(string projectPath, bool searchParentFolders)
{
DirectoryInfo directoryInfo = new DirectoryInfo(projectPath);
while (directoryInfo != null)
{
string gitDir = Path.Combine(directoryInfo.FullName, ".git");
string headFile = Path.Combine(gitDir, "HEAD");
if (Directory.Exists(gitDir) && File.Exists(headFile))
{
string headContent = File.ReadAllText(headFile).Trim();
int pos = headContent.LastIndexOf('/') + 1;
return (pos < headContent.Length) ? headContent.Substring(pos) : headContent;
}
if (!searchParentFolders)
{
break;
}
directoryInfo = directoryInfo.Parent;
}
return null;
}
public static string ReadPlasticBranchInfo(string projectPath, bool searchParentFolders)
{
string branchName = null;
DirectoryInfo directoryInfo = new DirectoryInfo(projectPath);
while (directoryInfo != null)
{
string plasticSelectorPath = Path.Combine(directoryInfo.FullName, ".plastic", "plastic.selector");
if (File.Exists(plasticSelectorPath))
{
branchName = ExtractPlasticBranch(plasticSelectorPath);
if (!string.IsNullOrEmpty(branchName))
{
return branchName;
}
}
if (!searchParentFolders)
{
break;
}
directoryInfo = directoryInfo.Parent;
}
return branchName;
}
private static string ExtractPlasticBranch(string plasticSelectorPath)
{
string[] lines = File.ReadAllLines(plasticSelectorPath);
foreach (string line in lines)
{
string trimmedLine = line.Trim();
if (trimmedLine.StartsWith("br ") || trimmedLine.StartsWith("smartbranch "))
{
// Extract the first quoted string
var match = Regex.Match(trimmedLine, "\"([^\"]+)\"");
if (match.Success)
{
string branchName = match.Groups[1].Value;
// Remove leading slash if present (e.g., "/main" becomes "main")
if (branchName.StartsWith("/"))
{
branchName = branchName.Substring(1);
}
return branchName;
}
}
}
return null;
}
static string GetTargetPlatformRaw(string projectPath)
{
string results = null;
// get buildtarget from .csproj
// StandaloneWindows64:19
// get main csproj file
var csproj = Path.Combine(projectPath, "Assembly-CSharp.csproj");
// TODO check projname also, if no assembly-.., NOTE already checked above
// var csproj = Path.Combine(projectPath, projectName + ".csproj");
if (File.Exists(csproj))
{
// Read the file line by line for performance
using (var reader = new StreamReader(csproj))
{
string line;
while ((line = reader.ReadLine()) != null)
{
const string tagStart = "";
const string tagEnd = "";
int startIdx = line.IndexOf(tagStart);
if (startIdx >= 0)
{
int endIdx = line.IndexOf(tagEnd, startIdx);
if (endIdx > startIdx)
{
string inner = line.Substring(startIdx + tagStart.Length, endIdx - startIdx - tagStart.Length);
int colonIndex = inner.IndexOf(':');
if (colonIndex > -1)
{
//Console.WriteLine("build target: " + inner.Substring(0, colonIndex));
// 5.6 : win32, win64, osx, linux, linux64, ios, android, web, webstreamed, webgl, xboxone, ps4, psp2, wsaplayer, tizen, samsungtv
// 2017: standalone, Win, Win64, OSXUniversal, Linux, Linux64, LinuxUniversal, iOS, Android, Web, WebStreamed, WebGL, XboxOne, PS4, PSP2, WindowsStoreApps, Switch, WiiU, N3DS, tvOS, PSM
// 2018: standalone, Win, Win64, OSXUniversal, Linux, Linux64, LinuxUniversal, iOS, Android, Web, WebStreamed, WebGL, XboxOne, PS4, WindowsStoreApps, Switch, N3DS, tvOS
// 2019: Standalone, Win, Win64, OSXUniversal, Linux64, iOS, Android, WebGL, XboxOne, PS4, WindowsStoreApps, Switch, tvOS
// 2020: Standalone, Win, Win64, OSXUniversal, Linux64, iOS, Android, WebGL, XboxOne, PS4, WindowsStoreApps, Switch, tvOS
// 2021: Standalone, Win, Win64, OSXUniversal, Linux64, iOS, Android, WebGL, XboxOne, PS4, WindowsStoreApps, Switch, tvOS
results = inner.Substring(0, colonIndex);
//results = (Platform)Enum.Parse(typeof(Platform), inner.Substring(0, colonIndex));
break; // we found it, exit early
}
}
}
}
}
}
else
{
//Console.WriteLine("Missing csproj, cannot parse target platform: " + projectPath);
}
return results;
}
public static string GetTargetPlatform(string projectPath)
{
var rawPlatformName = GetTargetPlatformRaw(projectPath);
return GetTargetPlatformFromRaw(rawPlatformName);
}
public static string GetTargetPlatformFromRaw(string rawPlatformName)
{
if (string.IsNullOrEmpty(rawPlatformName) == false && GetProjects.remapPlatformNames.ContainsKey(rawPlatformName))
{
return GetProjects.remapPlatformNames[rawPlatformName];
}
else
{
if (string.IsNullOrEmpty(rawPlatformName) == false) Console.WriteLine("Missing buildTarget remap name for: " + rawPlatformName);
return null;
}
}
public static string ReadCustomProjectData(string projectPath, string customFile)
{
// ProjectSettings is the old deprecated location (and current location from Hub 3.15.x, but later they will use UserSettings first too)
string[] directories = { "UserSettings", "ProjectSettings" };
foreach (var directory in directories)
{
string filePath = Path.Combine(projectPath, directory, customFile);
if (File.Exists(filePath))
{
return string.Join(" ", File.ReadAllLines(filePath));
}
}
return null;
}
public static bool SaveCustomProjectData(string projectPath, string customFile, string data)
{
// NOTE: now saving into UserSettings folder only (there might be old custom data is in ProjectSettings/ still..)
var customPath = Path.Combine(projectPath, "UserSettings");
if (!Directory.Exists(customPath))
{
Directory.CreateDirectory(customPath);
}
customFile = Path.Combine(customPath, customFile);
try
{
File.WriteAllText(customFile, data);
return true;
}
catch (Exception)
{
SetStatus("Failed to save custom project data to: " + customFile);
}
return false;
}
public static bool HasFocus(DependencyObject obj, Control control, bool checkChildren)
{
var oFocused = FocusManager.GetFocusedElement(obj) as DependencyObject;
if (!checkChildren)
return oFocused == control;
while (oFocused != null)
{
if (oFocused == control)
return true;
oFocused = VisualTreeHelper.GetParent(oFocused);
}
return false;
}
public static void SetFocusToGrid(DataGrid targetGrid, int index = -1)
{
if (targetGrid.Items.Count < 1) return;
if (index == -1 && targetGrid.SelectedIndex > -1) index = targetGrid.SelectedIndex;
if (index == -1) index = 0;
targetGrid.SelectedIndex = index;
// Try get the row, if not realized yet, defer
DataGridRow row = (DataGridRow)targetGrid.ItemContainerGenerator.ContainerFromIndex(index);
if (row == null)
{
// clamp to max items
if (index >= targetGrid.Items.Count) index = targetGrid.Items.Count - 1;
targetGrid.ScrollIntoView(targetGrid.Items[index]);
// Defer the focus once row is generated
targetGrid.Dispatcher.InvokeAsync(() =>
{
var newRow = (DataGridRow)targetGrid.ItemContainerGenerator.ContainerFromIndex(index);
if (newRow != null)
{
newRow.MoveFocus(new TraversalRequest(FocusNavigationDirection.Up));
newRow.Focus();
Keyboard.Focus(newRow);
}
}, DispatcherPriority.Background);
}
else
{
row.MoveFocus(new TraversalRequest(FocusNavigationDirection.Up));
row.Focus();
Keyboard.Focus(row);
}
}
public static string BrowseForOutputFolder(string title, string initialDirectory = null)
{
// https://stackoverflow.com/a/50261723/5452781
// Create a "Save As" dialog for selecting a directory (HACK)
var dialog = new SaveFileDialog();
if (initialDirectory != null) dialog.InitialDirectory = initialDirectory;
dialog.Title = title;
dialog.Filter = "Project Folder|*.Folder"; // Prevents displaying files
dialog.FileName = "Project"; // Filename will then be "select.this.directory"
if (dialog.ShowDialog() == true)
{
string path = dialog.FileName;
// Remove fake filename from resulting path
path = path.Replace("\\Project.Folder", "");
path = path.Replace("Project.Folder", "");
// If user has changed the filename, create the new directory
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
return path;
}
return null;
}
// TODO too many params..
public static Project FastCreateProject(string version, string baseFolder, string projectName = null, string templateZipPath = null, string[] platformsForThisUnity = null, string platform = null, bool useInitScript = false, string initScriptPath = null, bool forceDX11 = false)
{
// check for base folders in settings tab
if (string.IsNullOrEmpty(baseFolder) == true)
{
SetStatus("Missing baseFolder value");
return null;
}
// check if base folder exists
if (Directory.Exists(baseFolder) == false)
{
// TODO add streaming filter
SetStatus("Missing baseFolder: " + baseFolder);
return null;
}
// check selected unity version
if (string.IsNullOrEmpty(version) == true)
{
SetStatus("Missing unity version string");
return null;
}
string newPath = null;
// if we didnt have name yet
if (string.IsNullOrEmpty(projectName) == true)
{
projectName = GetSuggestedProjectName(version, baseFolder);
// failed getting new path a-z
if (projectName == null) return null;
}
newPath = Path.Combine(baseFolder, projectName);
// create folder
CreateEmptyProjectFolder(newPath, version);
bool cloneFromTemplate = false;
// unzip or copy template
if (templateZipPath != null)
{
// Console.WriteLine(templateZipPath);
if (File.Exists(templateZipPath))
{
cloneFromTemplate = true;
try
{
// NOTE no need to extract, unity can handle it with -cloneFromTemplate
//TarLib.Tar.ExtractTarGz(templateZipPath, newPath);
}
catch (Exception ex)
{
SetStatus("Error extract template file:" + ex.Message);
}
}
else if (Directory.Exists(templateZipPath))
{
// this is for folder templates, copy files
try
{
CopyDirectory(templateZipPath, newPath);
}
catch (Exception ex)
{
SetStatus("Error copying template folder: " + ex.Message);
}
}
}
// copy init file into project
if (useInitScript == true)
{
if (File.Exists(initScriptPath) == true)
{
var editorTargetFolder = Path.Combine(baseFolder, projectName, "Assets", "Editor");
if (Directory.Exists(editorTargetFolder) == false) Directory.CreateDirectory(editorTargetFolder);
var targetScriptFile = Path.Combine(editorTargetFolder, Path.GetFileName(initScriptPath));
// TODO overwrite old file, there shouldnt be anything here
if (File.Exists(targetScriptFile) == false) File.Copy(initScriptPath, targetScriptFile);
}
}
// launch empty project
var proj = new Project();
proj.Title = projectName;
proj.Path = Path.Combine(baseFolder, newPath).Replace("\\", "/");
proj.Version = version;
proj.TargetPlatforms = platformsForThisUnity;
proj.TargetPlatform = platform;
proj.Modified = DateTime.Now;
proj.folderExists = true; // have to set this value, so item is green on list
proj.Arguments = version.Contains("6000") ? (forceDX11 ? "-force-d3d11" : null) : null; // this gets erased later, since its not saved? would be nice to not add it at all though
if (cloneFromTemplate == true)
{
proj.Arguments += " -cloneFromTemplate \"" + templateZipPath + "\"";
}
var proc = LaunchProject(proj, null, useInitScript, false, cloneFromTemplate);
ProcessHandler.Add(proj, proc);
return proj;
} // FastCreateProject
public static string GetSuggestedProjectName(string version, string baseFolder)
{
// check for base folders in settings tab, could use currently selected project folder parent as base?
if (string.IsNullOrEmpty(baseFolder))
{
Console.WriteLine("Missing txtRootFolderForNewProjects");
return null;
}
// find next free folder checking all "unityversion_a-z" characters
var unityBaseVersion = version.Substring(0, version.LastIndexOf('.'));
unityBaseVersion = unityBaseVersion.Replace(".", "_");
for (int i = 97; i < 122; i++)
{
var newProject = unityBaseVersion + "_" + ((char)i);
var path = Path.Combine(baseFolder, newProject);
if (Directory.Exists(path))
{
//Console.WriteLine("directory exists..trying again");
}
else // its available
{
return newProject;
}
}
// couldnt find free letter to use, lets add timestamp then
return unityBaseVersion + "_" + DateTime.Now.ToString("ddMMyyyy_HHmmss");
}
static void CreateEmptyProjectFolder(string path, string version)
{
Console.WriteLine("Create new project folder: " + path);
// TODO add streaming filter
SetStatus("Creating new project folder: " + path);
Directory.CreateDirectory(path);
// create project version file, to avoid wrong version warning
var settingsPath = Path.Combine(path, "ProjectSettings");
Directory.CreateDirectory(settingsPath);
var settingsFile = Path.Combine(settingsPath, "ProjectVersion.txt");
File.WriteAllText(settingsFile, "m_EditorVersion: " + version);
}
public static string GetGlobalEditorLogsFolder()
{
// NOTE this is pre 6.5 unity (in 6.5 and later its inside project: MyProj/Logs/Editor.log)
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Unity", "Editor");
}
public static void OpenEditorLogForProject(Project proj)
{
// if version is 6000.5 or higher, then log file is in Proj/Logs, otherwise in Appdata/LocalLow
if (proj.Version != null && Tools.IsVersionAtLeast(proj.Version, "6000.5") == true)
{
var logFolder = Path.Combine(proj.Path, "Logs");
var logFile = Path.Combine(logFolder, "Editor.log");
if (File.Exists(logFile) == true)
{
Tools.LaunchExe(logFile);
}
else
{
SetStatus("Log file not found: " + logFile, MessageType.Warning);
}
}
else
{
var logFolder = Tools.GetGlobalEditorLogsFolder();
var logFile = Path.Combine(logFolder, "Editor.log");
if (File.Exists(logFile) == true)
{
Tools.LaunchExe(logFile);
}
else
{
SetStatus("Log file not found: " + logFile, MessageType.Warning);
}
}
}
public static string[] GetPlatformsForUnityVersion(string version)
{
// get platforms array for this unity version
// TODO use dictionary instead of looping versions
for (int i = 0; i < MainWindow.unityInstallationsSource.Count; i++)
{
if (MainWindow.unityInstallationsSource[i].Version == version)
{
return MainWindow.unityInstallationsSource[i].Platforms;
}
}
return null;
}
// https://stackoverflow.com/a/675347/5452781
public static void SetStartupRegistry(bool state)
{
RegistryKey rk = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
if (state == true)
{
rk.SetValue(MainWindow.appName, "\"" + Process.GetCurrentProcess().MainModule.FileName + "\"");
}
else
{
rk.DeleteValue(MainWindow.appName, false);
}
}
public static Dictionary ScanTemplates(string unityInstallPath)
{
var items = new Dictionary();
// add none as default item
items.Add("None", null);
// get list of existing packages
var unityPath = Path.GetDirectoryName(unityInstallPath);
var templateFolder = Path.Combine(unityPath, "Data/Resources/PackageManager/ProjectTemplates/");
if (Directory.Exists(templateFolder) == false) return items;
// get all files in template folder
var fileEntries = Directory.GetFiles(templateFolder).ToList();
// process found files
for (int i = 0; i < fileEntries.Count; i++)
{
//Console.WriteLine(fileEntries[i]);
// check if its tgz
if (fileEntries[i].ToLower().IndexOf(".tgz") > -1)
{
// cleanup displayed name
var name = Path.GetFileName(fileEntries[i]).Replace("com.unity.template.", "").Replace(".tgz", "");
items.Add(name, fileEntries[i]);
}
}
// 2018.4 has folders instead of tgz files
// BUT do this for all versions, in case user has added custom template folders (that contain Assets/ folder)
var dirEntries = Directory.GetDirectories(templateFolder).ToList();
for (int i = 0; i < dirEntries.Count; i++)
{
//Console.WriteLine(dirEntries[i]);
// if it has com.unity.template. prefix, then its a template
// if that directory contains Assets/ folder, then its a template
if (Directory.Exists(Path.Combine(dirEntries[i], "Assets")) == true)
{
//Console.WriteLine(dirEntries[i]);
var name = Path.GetFileName(dirEntries[i]).Replace("com.unity.template.", "");
items.Add(name, dirEntries[i]);
}
}
return items;
}
// chatgpt
public static string GetElapsedTime(DateTime datetime)
{
TimeSpan ts = DateTime.Now - datetime;
if (ts.TotalSeconds < 60)
{
return ts.TotalSeconds < 2 ? "Right now" : $"{(int)ts.TotalSeconds} seconds ago";
}
else if (ts.TotalMinutes < 60)
{
return ts.TotalMinutes < 2 ? "1 minute ago" : $"{(int)ts.TotalMinutes} minutes ago";
}
else if (ts.TotalHours < 24)
{
return ts.TotalHours < 2 ? "1 hour ago" : $"{(int)ts.TotalHours} hours ago";
}
else if (ts.TotalDays < 30)
{
return ts.TotalDays < 2 ? "1 day ago" : $"{(int)ts.TotalDays} days ago";
}
else if (ts.TotalDays < 365)
{
if (ts.TotalDays < 60)
{
return "1 month ago";
}
else
{
return $"{(int)(ts.TotalDays / 30)} months ago";
}
}
else
{
return ts.TotalDays < 730 ? "1 year ago" : $"{(int)(ts.TotalDays / 365)} years ago";
}
}
public static bool ValidateDateFormat(string format)
{
try
{
String formattedDate = DateTime.Now.ToString(format);
DateTime.Parse(formattedDate);
return true;
}
catch (Exception)
{
//Console.WriteLine("Invalid custom datetime format: " + format);
return false;
}
}
// https://stackoverflow.com/a/37724335/5452781
public static void BringProcessToFront(Process process)
{
if (process == null) return;
IntPtr handle = process.MainWindowHandle;
if (IsIconic(handle))
{
ShowWindow(handle, SW_RESTORE);
}
SetForegroundWindow(handle);
}
public static void DownloadAdditionalModules(string UnityExePath, string unityVersion, string moduleName)
{
var editorFolder = Path.GetDirectoryName(UnityExePath);
string hash = null;
// get from unity exe (only for 2018.4 and later?)
var versionInfo = FileVersionInfo.GetVersionInfo(UnityExePath);
var versionRaw = versionInfo.ProductVersion.Split('_');
if (versionRaw.Length == 2)
{
hash = versionRaw[1];
}
else // try other files
{
var changeSetFile = Path.Combine(editorFolder, @"Data\PlaybackEngines\windowsstandalonesupport\Source\WindowsPlayer\WindowsPlayer\UnityConfigureRevision.gen.h");
if (File.Exists(changeSetFile) == true)
{
var allText = File.ReadAllText(changeSetFile);
var hashRaw = allText.Split(new string[] { "#define UNITY_VERSION_HASH \"" }, StringSplitOptions.None);
if (hashRaw.Length > 1)
{
hash = hashRaw[1].Replace("\"", "");
}
else
{
Console.WriteLine("Unable to parse UNITY_VERSION_HASH from " + changeSetFile);
}
}
else
{
Console.WriteLine("Changeset hash file not found: " + changeSetFile);
}
}
if (hash == null) return;
var moduleURL = "https://download.unity3d.com/download_unity/" + hash + "/TargetSupportInstaller/UnitySetup-" + moduleName + "-Support-for-Editor-" + unityVersion + ".exe";
OpenURL(moduleURL);
}
public static void OpenAppdataSpecialFolder(string subfolder)
{
var logfolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), subfolder);
if (Directory.Exists(logfolder) == true)
{
if (Tools.LaunchExplorer(logfolder) == false)
{
Console.WriteLine("Cannot open folder.." + logfolder);
}
}
}
// NOTE android only at the moment
public static void BuildProject(Project proj, Platform platform)
{
Console.WriteLine("Building " + proj.Title + " for " + platform);
SetStatus("Build process started: " + DateTime.Now.ToString("HH:mm:ss"));
// TODO use theme colors, keep list of multiple builds, if click status button show list of builds, if click for single build (show output folder)
SetBuildStatus(Colors.Red);
if (string.IsNullOrEmpty(proj.Path)) return;
// create builder script template (with template string, that can be replaced with project related paths or names?)
// copy editor build script to Assets/Editor/ folder (if already exists then what? Use UnityLauncherBuildSomething.cs name, so can overwrite..)
var editorScriptFolder = Path.Combine(proj.Path, "Assets", "Editor");
if (Directory.Exists(editorScriptFolder) == false) Directory.CreateDirectory(editorScriptFolder);
// TODO check if creation failed
// create output file for editor script
var editorScriptFile = Path.Combine(editorScriptFolder, "UnityLauncherProBuilder.cs");
// check build folder and create if missing
var outputFolder = Path.Combine(proj.Path, "Builds/" + platform + "/");
outputFolder = outputFolder.Replace('\\', '/'); // fix backslashes
Console.WriteLine("outputFolder= " + outputFolder);
if (Directory.Exists(outputFolder) == false) Directory.CreateDirectory(outputFolder);
// TODO check if creation failed
// cleanup filename from project name
var invalidChars = Path.GetInvalidFileNameChars();
var outputFile = String.Join("_", proj.Title.Split(invalidChars, StringSplitOptions.RemoveEmptyEntries)).TrimEnd('.');
// replace spaces also, for old time(r)s
outputFile = outputFile.Replace(' ', '_');
outputFile = Path.Combine(outputFolder, outputFile + ".apk");
Console.WriteLine("outputFile= " + outputFile);
// TODO move to txt resource? and later load from local custom file if exists, and later open window or add settings for build options
// TODO different unity versions? wont work in older unitys right now
var builderScript = @"using System.Linq;
using UnityEditor;
using UnityEngine;
public static class UnityLauncherProTools
{
public static void BuildAndroid()
{
EditorUserBuildSettings.buildAppBundle = false;
EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP);
PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARM64;
var settings = new BuildPlayerOptions();
settings.scenes = GetScenes();
settings.locationPathName = ""###OUTPUTFILE###"";
settings.target = BuildTarget.Android;
settings.options = BuildOptions.None;
var report = BuildPipeline.BuildPlayer(settings);
}
public static void BuildiOS() // Note need to match platform name
{
PlayerSettings.iOS.targetDevice = iOSTargetDevice.iPhoneAndiPad;
var settings = new BuildPlayerOptions();
settings.scenes = GetScenes();
settings.locationPathName = ""###OUTPUTFOLDER###"";
settings.target = BuildTarget.iOS;
settings.options = BuildOptions.None;
var report = BuildPipeline.BuildPlayer(settings);
}
static string[] GetScenes()
{
return EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(scene => scene.path).ToArray();
}
}";
// fill in project specific data
builderScript = builderScript.Replace("###OUTPUTFILE###", outputFile); // android
builderScript = builderScript.Replace("###OUTPUTFOLDER###", outputFolder); // ios
Console.WriteLine("builderScript=" + builderScript);
File.WriteAllText(editorScriptFile, builderScript);
// TODO check if write failed
// get selected project unity exe path
var unityExePath = Tools.GetUnityExePath(proj.Version);
if (unityExePath == null) return;
// create commandline string for building and launch it
//var buildcmd = $"\"{unityExePath}\" -quit -batchmode -nographics -projectPath \"{proj.Path}\" -executeMethod \"Builder.BuildAndroid\" -buildTarget android -logFile -";
// TODO test without nographics : https://forum.unity.com/threads/batch-build-one-scene-is-black-works-in-normal-file-build.1282823/#post-9456524
var buildParams = $" -quit -batchmode -nographics -projectPath \"{proj.Path}\" -executeMethod \"UnityLauncherProTools.Build{platform}\" -buildTarget {platform} -logFile \"{outputFolder}/../build.log\"";
Console.WriteLine("buildcmd= " + buildParams);
// launch build
var proc = Tools.LaunchExe(unityExePath, buildParams);
// wait for process exit then open output folder
proc.Exited += (o, i) =>
{
Console.WriteLine("Build process exited: " + outputFolder);
Tools.ExploreFolder(outputFolder);
SetStatus("Build process finished: " + DateTime.Now.ToString("HH:mm:ss"));
// TODO set color based on results
SetBuildStatus(Colors.Green);
};
}
public static void BuildProjectCustom(Project proj)
{
Console.WriteLine("Building " + proj.Title + " (custom)");
SetStatus("Build process started: " + DateTime.Now.ToString("HH:mm:ss"));
// get selected project unity exe path
var unityExePath = Tools.GetUnityExePath(proj.Version);
if (unityExePath == null) return;
// create commandline string for building and launch it
//var buildcmd = $"\"{unityExePath}\" -quit -batchmode -nographics -projectPath \"{proj.Path}\" -executeMethod \"Builder.BuildAndroid\" -buildTarget android -logFile -";
// TODO test without nographics : https://forum.unity.com/threads/batch-build-one-scene-is-black-works-in-normal-file-build.1282823/#post-9456524
var buildParams = $" -quit -batchmode -nographics -projectPath \"{proj.Path}\" -executeMethod \"UnityLauncherProToolsCustom.BuildCustom\"";
Console.WriteLine("buildcmd= " + buildParams);
// launch build
var proc = Tools.LaunchExe(unityExePath, buildParams);
// wait for process exit then open output folder
proc.Exited += (o, i) =>
{
SetStatus("Build process finished: " + DateTime.Now.ToString("HH:mm:ss"));
// TODO set color based on results
SetBuildStatus(Colors.Green);
};
}
// runs unity SimpleWebServer.exe and launches default Browser into project build/ folder'
public static void LaunchWebGL(Project proj, string relativeFolder)
{
var projPath = proj?.Path.Replace('/', '\\');
if (string.IsNullOrEmpty(projPath) == true) return;
var buildPath = Path.Combine(projPath, "Builds", relativeFolder);
if (Directory.Exists(buildPath) == false) return;
if (MainWindow.unityInstalledVersions.ContainsKey(proj.Version) == false) return;
// get mono and server exe paths
var editorPath = Path.GetDirectoryName(MainWindow.unityInstalledVersions[proj.Version]);
var monoToolsPath = Path.Combine(editorPath, "Data/MonoBleedingEdge/bin");
if (Directory.Exists(monoToolsPath) == false) return;
var webglToolsPath = Path.Combine(editorPath, "Data/PlaybackEngines/WebGLSupport/BuildTools");
if (Directory.Exists(webglToolsPath) == false) return;
var monoExe = Path.Combine(monoToolsPath, "mono.exe");
if (File.Exists(monoExe) == false) return;
var webExe = Path.Combine(webglToolsPath, "SimpleWebServer.exe");
if (File.Exists(webExe) == false) return;
int port = MainWindow.webglPort;
if (port < 50000) port = 50000;
if (port > 65534) port = 65534;
// check if this project already has server running and process is not closed
if (webglServerProcesses.ContainsKey(port) && webglServerProcesses[port].HasExited == false)
{
Console.WriteLine("Port found in cache: " + port + " process=" + webglServerProcesses[port]);
// check if project matches
if (webglServerProcesses[port].StartInfo.Arguments.IndexOf("\"" + buildPath + "\"") > -1)
{
Console.WriteLine("this project already has webgl server running.. lets open browser url only");
// then open browser url only
Tools.OpenURL("http://localhost:" + port);
return;
}
else
{
Console.WriteLine("Port in use, but its different project: " + port);
Console.WriteLine(webglServerProcesses[port].StartInfo.Arguments + " == " + "\"" + buildPath + "\"");
// then open new port and process
// -----------------------------------------------------------
// check if port is available https://stackoverflow.com/a/2793289
bool isAvailable = true;
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
IPEndPoint[] objEndPoints = ipGlobalProperties.GetActiveTcpListeners();
// NOTE instead of iterating all ports, just try to open port, if fails, open next one
// compare with existing ports, if available
for (int i = 0; i < objEndPoints.Length; i++)
{
if (objEndPoints[i].Port == port)
{
port++;
if (port > 65535)
{
Console.WriteLine("Failed to find open port..");
isAvailable = false;
return;
}
}
}
Console.WriteLine("Found available port: " + port);
if (isAvailable == false)
{
Console.WriteLine("failed to open port " + port + " (should be open already, or something else is using it?)");
}
else
{
// take process id from unity, if have it (then webserver closes automatically when unity is closed)
var proc = ProcessHandler.Get(proj.Path);
int pid = proc == null ? -1 : proc.Id;
string param = null;
// parse proj version year as number 2019.4.1f1 -> 2019
int year = 0;
var versionParts = proj.Version.Split('.');
bool parsedYear = int.TryParse(versionParts[0], out year);
if (parsedYear && year >= 6000)
{
param = "\"" + webExe + "\" \"" + buildPath + "\" " + "http://localhost:" + port + "/" + (pid == -1 ? "" : " " + pid);
}
else // older versions or failed to parse
{
param = "\"" + webExe + "\" \"" + buildPath + "\" " + port + (pid == -1 ? "" : " " + pid); // server exe path, build folder and port
}
var webglServerProcess = Tools.LaunchExe(monoExe, param);
if (webglServerProcesses.ContainsKey(port))
{
Console.WriteLine("Error> Should not happen - this port is already in dictionary! port: " + port);
}
else // keep reference to this process on this port
{
// TODO how to remove process once its closed? (or unlikely to have many processes in total? can also remove during check, if process already null)
webglServerProcesses.Add(port, webglServerProcess);
Console.WriteLine("Added port " + port);
}
Tools.OpenURL("http://localhost:" + port);
}
// -----------------------------------------------------------
}
}
else
{
Console.WriteLine("Port not running in cache or process already closed, remove it from cache: " + port);
if (webglServerProcesses.ContainsKey(port)) webglServerProcesses.Remove(port);
// TODO remove duplicate code
// then open new process
// -----------------------------------------------------------
// check if port is available https://stackoverflow.com/a/2793289
bool isAvailable = true;
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
IPEndPoint[] objEndPoints = ipGlobalProperties.GetActiveTcpListeners();
// compare with existing ports, if available
for (int i = 0; i < objEndPoints.Length; i++)
{
if (objEndPoints[i].Port == port)
{
if (port > 65535)
{
Console.WriteLine("Failed to find open port..");
isAvailable = false;
return;
}
port++;
}
}
Console.WriteLine("Found available port: " + port);
if (isAvailable == false)
{
Console.WriteLine("failed to open port " + port + " (should be open already, or something else is using it?)");
}
else
{
// take process id from unity, if have it(then webserver closes automatically when unity is closed)
var proc = ProcessHandler.Get(proj.Path);
int pid = proc == null ? -1 : proc.Id;
// parse proj version year as number 2019.4.1f1 -> 2019
string param = null;
int year = 0;
var versionParts = proj.Version.Split('.');
bool parsedYear = int.TryParse(versionParts[0], out year);
if (parsedYear && year >= 6000)
{
param = "\"" + webExe + "\" \"" + buildPath + "\" " + "\"http://localhost:" + port + "/\"" + (pid == -1 ? "" : " " + pid);
}
else // older versions or failed to parse
{
param = "\"" + webExe + "\" \"" + buildPath + "\" " + port + (pid == -1 ? "" : " " + pid); // server exe path, build folder and port
}
//var param = "\"" + webExe + "\" \"" + buildPath + "\" " + port + (pid == -1 ? "" : " " + pid); // server exe path, build folder and port
var webglServerProcess = Tools.LaunchExe(monoExe, param);
if (webglServerProcess == null)
{
Console.WriteLine("Failed to start exe..");
}
if (webglServerProcesses.ContainsKey(port))
{
Console.WriteLine("Error> Should not happen - this port is already in dictionary! port: " + port);
}
else // keep reference to this process on this port
{
// TODO how to remove process once its closed? (or unlikely to have many processes in total? can also remove during check, if process already null)
webglServerProcesses.Add(port, webglServerProcess);
Console.WriteLine("Added port " + port);
}
Tools.OpenURL("http://localhost:" + port);
}
// -----------------------------------------------------------
}
} // LaunchWebGL()
// creates .bat file to launch UnityLauncherPro and then .url link file on desktop, into that .bat file
public static bool CreateDesktopShortCut(Project proj, string batchFolder)
{
string lnkFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop));
if (string.IsNullOrEmpty(batchFolder)) return false;
//string batchFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "UnityLauncherPro");
if (Directory.Exists(batchFolder) == false) Directory.CreateDirectory(batchFolder);
string batFileName = Path.Combine(batchFolder, proj.Title + ".bat");
string launcherExe = Process.GetCurrentProcess().MainModule.FileName;
string args = "-projectPath " + "\"" + proj.Path + "\" " + proj.Arguments;
string description = "Unity Project: " + proj.Title;
// create .bat file
var batLauncherData = "start \"\" \"" + launcherExe + "\"" + " " + args;
File.WriteAllText(batFileName, batLauncherData);
// create desktop link file
using (StreamWriter writer = new StreamWriter(lnkFileName + "\\" + proj.Title + ".url"))
{
writer.WriteLine("[InternetShortcut]");
writer.WriteLine("URL=file:///" + batFileName);
//writer.WriteLine("ShowCommand=7"); // doesnt work for minimized
writer.WriteLine("IconIndex=0");
writer.WriteLine("Arguments=-projectPath " + proj.Path);
// TODO maybe could take icon from project (but then need to convert into .ico)
string iconExe = GetUnityExePath(proj.Version);
if (iconExe == null) iconExe = launcherExe;
string icon = iconExe.Replace('\\', '/');
writer.WriteLine("IconFile=" + icon);
}
// TODO check for streamwriter and file write success
return true;
}
internal static long GetFolderSizeInBytes(string currentBuildReportProjectPath)
{
// FIXME: 0 is not really correct for missing folder..
if (Directory.Exists(currentBuildReportProjectPath) == false) return 0;
return DirSize(new DirectoryInfo(currentBuildReportProjectPath));
}
// https://stackoverflow.com/a/468131/5452781
static long DirSize(DirectoryInfo d)
{
long size = 0;
// Add file sizes.
FileInfo[] fis = d.GetFiles();
foreach (FileInfo fi in fis)
{
size += fi.Length;
}
// Add subdirectory sizes.
DirectoryInfo[] dis = d.GetDirectories();
foreach (DirectoryInfo di in dis)
{
size += DirSize(di);
}
return size;
}
// Returns the human-readable file size for an arbitrary, 64-bit file size
// The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB"
internal static string GetBytesReadable(long i)
{
// Get absolute value
long absolute_i = (i < 0 ? -i : i);
// Determine the suffix and readable value
string suffix;
double readable;
if (absolute_i >= 0x1000000000000000) // Exabyte
{
suffix = "EB";
readable = (i >> 50);
}
else if (absolute_i >= 0x4000000000000) // Petabyte
{
suffix = "PB";
readable = (i >> 40);
}
else if (absolute_i >= 0x10000000000) // Terabyte
{
suffix = "TB";
readable = (i >> 30);
}
else if (absolute_i >= 0x40000000) // Gigabyte
{
suffix = "GB";
readable = (i >> 20);
}
else if (absolute_i >= 0x100000) // Megabyte
{
suffix = "MB";
readable = (i >> 10);
}
else if (absolute_i >= 0x400) // Kilobyte
{
suffix = "KB";
readable = i;
}
else
{
return i.ToString("0 B"); // Byte
}
// Divide by 1024 to get fractional value
readable = (readable / 1024);
// Return formatted number with suffix
return readable.ToString("0.### ") + suffix;
}
public static MainWindow mainWindow;
// set status bar in main thread
public static void SetStatus(string text, MessageType messageType = MessageType.Info)
{
mainWindow.Dispatcher.Invoke(() => { mainWindow.SetStatus(text, messageType); });
}
public static void SetBuildStatus(Color color)
{
mainWindow.Dispatcher.Invoke(() => { mainWindow.SetBuildStatus(color); });
}
// https://unity3d.com/unity/alpha
public static bool IsAlpha(string version)
{
if (string.IsNullOrEmpty(version)) return false;
return version.IndexOf("a", 0, StringComparison.CurrentCultureIgnoreCase) > -1;
}
// https://unity3d.com/beta/
public static bool IsBeta(string version)
{
if (string.IsNullOrEmpty(version)) return false;
return version.IndexOf("b", 0, StringComparison.CurrentCultureIgnoreCase) > -1;
}
// https://unity3d.com/unity/qa/lts-releases
public static bool IsLTS(string versionRaw)
{
if (string.IsNullOrEmpty(versionRaw)) return false;
var version = versionRaw.Split('.');
var versionInt = int.Parse(version[0]);
var versionMinor = int.Parse(version[1]);
return (versionInt >= 2017 && versionMinor == 4) || (versionInt > 2019 && versionMinor == 3);
}
internal static void UninstallEditor(string path, string version)
{
if (string.IsNullOrEmpty(path)) return;
if (string.IsNullOrEmpty(version)) return;
// run uninstaller from path
var installFolder = Path.GetDirectoryName(path);
var uninstaller = Path.Combine(installFolder, "Uninstall.exe");
// TODO could be optional setting for non-silent uninstall
LaunchExe(uninstaller, "/S");
// remove firewall settings
var cmd = "netsh advfirewall firewall delete rule name=all program=\"" + path + "\"";
Console.WriteLine("Cleanup firewall: " + cmd);
LaunchExe("cmd.exe", "/c " + cmd);
int year;
string[] parts = version.Split('.');
// TODO handle unity 6.x
if (parts.Length >= 1 && int.TryParse(parts[0], out year) && year cancellationTokenSource.Cancel());
progressWindow.Show();
var result = false;
// create destination folder, if not exists
var outputFolderPath = Path.GetDirectoryName(destinationPath);
if (Directory.Exists(outputFolderPath) == false)
{
Directory.CreateDirectory(outputFolderPath);
}
try
{
using (var client = new HttpClient())
using (var response = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token))
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? 1;
var buffer = new byte[8192];
var totalRead = 0;
using (var contentStream = await response.Content.ReadAsStreamAsync())
using (var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, buffer.Length, true))
{
int bytesRead;
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationTokenSource.Token)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationTokenSource.Token);
totalRead += bytesRead;
progressWindow.UpdateProgress(new DownloadProgress(totalRead, totalBytes));
}
result = true;
}
}
}
catch (TaskCanceledException)
{
Console.WriteLine("Download cancelled");
}
catch (Exception e)
{
Console.WriteLine(e);
}
finally
{
if (!result)
{
InstallationCompleted(destinationPath);
}
progressWindow.Close();
}
return result;
}
internal static string GetSRP(string projectPath)
{
var settingsFile = Path.Combine(projectPath, "ProjectSettings", "GraphicsSettings.asset");
if (!File.Exists(settingsFile)) return null;
bool srpSectionFound = false;
using (var reader = new StreamReader(settingsFile))
{
string line;
while ((line = reader.ReadLine()) != null)
{
if (!srpSectionFound)
{
if (line.Contains("m_SRPDefaultSettings:") || line.Contains("m_RenderPipelineGlobalSettingsMap:"))
{
srpSectionFound = true;
}
continue;
}
if (line.Contains("UniversalRenderPipeline"))
{
return "URP";
}
else if (line.Contains("HDRenderPipeline"))
{
return "HDRP";
}
}
}
return null; // BIRP or unknown
}
internal static void InstallAPK(string ApkPath)
{
try
{
var cmd = "cmd.exe";
var pars = $"/C adb install -r \"{ApkPath}\""; // Use /C to execute and close the window after finishing
var processStartInfo = new ProcessStartInfo
{
FileName = cmd,
Arguments = pars,
RedirectStandardOutput = true, // Capture output to wait for completion
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = false
};
string installOutput = null;
string installError = null;
using (var installProcess = Process.Start(processStartInfo))
{
installOutput = installProcess.StandardOutput.ReadToEnd();
installError = installProcess.StandardError.ReadToEnd();
installProcess.WaitForExit();
if (installProcess.ExitCode != 0 || !string.IsNullOrEmpty(installError))
{
SetStatus($"Error installing APK: {installError.Trim()}\n{installOutput.Trim()}");
return;
}
}
// Attempt to extract package name using aapt
var aaptCmd = $"aapt dump badging \"{ApkPath}\" | findstr package:";
var aaptProcessStartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/C {aaptCmd}",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
string packageName = null;
using (var process = Process.Start(aaptProcessStartInfo))
{
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
var match = System.Text.RegularExpressions.Regex.Match(output, "package: name='(.*?)'");
if (match.Success)
{
packageName = match.Groups[1].Value;
}
}
if (!string.IsNullOrEmpty(packageName))
{
// Run the application using adb
var runPars = $"/C adb shell monkey -p {packageName} 1";
var runProcessStartInfo = new ProcessStartInfo
{
FileName = cmd,
Arguments = runPars,
UseShellExecute = true,
CreateNoWindow = false,
WindowStyle = ProcessWindowStyle.Normal
};
Process.Start(runProcessStartInfo);
SetStatus($"Installed and launched APK with package name: {packageName}");
}
else
{
SetStatus("ADB install completed, but failed to extract package name. Application not launched.");
}
}
catch (Win32Exception ex)
{
// Handle case where adb or aapt is not found
SetStatus($"Error: Required tool not found. Ensure adb and aapt are installed and added to PATH. Details: {ex.Message}");
}
catch (Exception ex)
{
// Handle other unexpected exceptions
SetStatus($"An unexpected error occurred: {ex.Message}");
}
}
// exclude folders from windows defender
internal static bool RunExclusionElevated(IEnumerable paths, bool silent = false)
{
var escapedPaths = new List();
foreach (var rawPath in paths)
{
var path = rawPath.Trim();
string safePath = path.Replace("'", "''");
escapedPaths.Add($"'{safePath}'");
}
string joinedPaths = string.Join(", ", escapedPaths);
string psCommand = $"Add-MpPreference -ExclusionPath {joinedPaths}";
string fullCommand;
if (silent)
{
// No output, just run the command silently
fullCommand = psCommand;
}
else
{
// Show command and keep window open
var quotedPathsForDisplay = string.Join(", ", escapedPaths.ConvertAll(p => $"'{p.Trim('\'')}'"));
string displayCommand = $"Add-MpPreference -ExclusionPath {quotedPathsForDisplay}";
fullCommand = $"Write-Host 'Running: {displayCommand}'; {psCommand}; Write-Host ''; Write-Host 'Done. Press any key to exit...'; pause";
}
var startInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = silent
? $"-WindowStyle Hidden -Command \"{fullCommand}\""
: $"-NoExit -Command \"{fullCommand}\"",
UseShellExecute = true,
Verb = "runas", // Requires admin rights
WindowStyle = silent ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal
};
try
{
Process.Start(startInfo);
}
catch (Win32Exception)
{
if (!silent)
{
MessageBox.Show("Operation cancelled or failed due to insufficient privileges.", "Cancelled", MessageBoxButton.OK, MessageBoxImage.Warning);
}
return false;
}
catch (Exception ex)
{
MessageBox.Show("Error: " + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
return true;
}
///
/// Returns a full file path under either the application's install folder
/// or, if that isn't writable, under LocalAppData\UnityLauncherPro\.
///
/// e.g. "Themes", "Scripts"
/// e.g. "custom.ini", "InitializeProject.cs"
public static string GetSafeFilePath(string subfolder, string fileName)
{
// 1) Preferred: under the app folder
string preferredDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, subfolder);
// 2) Fallback: in LocalAppData
string fallbackDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "UnityLauncherPro", subfolder);
try
{
// Safe even if it already exists
Directory.CreateDirectory(preferredDir);
return Path.Combine(preferredDir, fileName);
}
catch (UnauthorizedAccessException)
{
// no write access under Program Files
}
catch (Exception ex)
{
// optional: log unexpected errors
Console.WriteLine($"Warning: couldnt create {preferredDir}: {ex.Message}");
}
// Ensure fallback always exists
Directory.CreateDirectory(fallbackDir);
return Path.Combine(fallbackDir, fileName);
}
// copy directory structure (for template folder)
public static void CopyDirectory(string sourceDir, string targetDir)
{
// Create the target directory if it doesn't exist
Directory.CreateDirectory(targetDir);
// Copy all files
foreach (var file in Directory.GetFiles(sourceDir))
{
var destFile = Path.Combine(targetDir, Path.GetFileName(file));
File.Copy(file, destFile, overwrite: false);
}
// Recursively copy subdirectories
foreach (var directory in Directory.GetDirectories(sourceDir))
{
var destDir = Path.Combine(targetDir, Path.GetFileName(directory));
CopyDirectory(directory, destDir);
}
}
// test fetching
// Reuse a single HttpClient
private static readonly HttpClient _http = new HttpClient
{
Timeout = TimeSpan.FromSeconds(15)
};
///
/// Fire-and-forget entry; returns immediately. Keeps your UI thread free.
///
internal static void FetchAdditionalInfoForEditors()
{
_ = FetchImplAsync(); // don't await -> no UI hang
}
private static async Task FetchImplAsync()
{
try
{
foreach (var version in MainWindow.unityInstalledVersions.Keys)
{
//Console.WriteLine("******** Fetching " + version);
var url = $"https://services.api.unity.com/unity/editor/release/v1/releases?order=RELEASE_DATE_DESC&limit=1&version={Uri.EscapeDataString(version)}";
string json;
try
{
json = await _http.GetStringAsync(url).ConfigureAwait(false);
}
catch (Exception ex)
{
Debug.WriteLine($"[FetchAdditionalInfo] {version}: HTTP error: {ex.Message}");
continue;
}
try
{
//Console.WriteLine("received: " + json);
UnityEditorInfoLabel label = ExtractLabelText(json);
if (label != null)
{
// feed this info back to main window
if (string.IsNullOrEmpty(label.labelText) == false)
{
//Console.WriteLine(label.description);
var u = MainWindow.unityInstallationsSource.FirstOrDefault(x => x.Version == version);
string infoText = label.labelText + "\n" + label.description; ;
u.InfoLabel = infoText;
int index = MainWindow.unityInstallationsSource.IndexOf(u);
if (index >= 0)
{
MainWindow.unityInstallationsSource[index] = u;
}
// update all projectsSource items with this version too
foreach (var p in MainWindow.projectsSource)
{
if (p.Version == version)
{
p.InfoLabel = infoText;
}
}
//SetStatus($"Info for {version}: {label.labelText}");
//Console.WriteLine("got infolabel for " + version);
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"[FetchAdditionalInfo] {version}: JSON parse error: {ex.Message}");
}
// delay
await Task.Delay(500).ConfigureAwait(false);
} // foreach version
}
catch (Exception ex)
{
// Last-resort catch so fire-and-forget exceptions arent unobserved
Debug.WriteLine($"[FetchAdditionalInfo] Fatal: {ex}");
}
} // FetchImplAsync()
public class UnityEditorInfoLabel
{
public string description; // may contain HTML
public string labelText;
public string icon;
public string color;
}
private static UnityEditorInfoLabel ExtractLabelText(string json)
{
var label = new UnityEditorInfoLabel();
if (string.IsNullOrEmpty(json) || json.Contains("\"results\":[]")) return label;
int resultsIndex = json.IndexOf("\"results\":", StringComparison.Ordinal);
if (resultsIndex == -1) return label;
// Use the LAST "label": in the payload (the editor-level one)
int labelIndex = json.LastIndexOf("\"label\":", StringComparison.Ordinal);
if (labelIndex == -1 || labelIndex < resultsIndex) return label;
// labelText
int ltIndex = json.IndexOf("\"labelText\":", labelIndex, StringComparison.Ordinal);
if (ltIndex != -1)
{
int vStart = json.IndexOf('\"', ltIndex + 12) + 1; // "labelText": => +12
if (vStart > 0)
{
int vEnd = json.IndexOf('\"', vStart);
if (vEnd > vStart) label.labelText = json.Substring(vStart, vEnd - vStart);
}
}
// icon
int iconIndex = json.IndexOf("\"icon\":", labelIndex, StringComparison.Ordinal);
if (iconIndex != -1)
{
int vStart = json.IndexOf('\"', iconIndex + 7) + 1; // "icon": => +7
if (vStart > 0)
{
int vEnd = json.IndexOf('\"', vStart);
if (vEnd > vStart) label.icon = json.Substring(vStart, vEnd - vStart);
}
}
// color
int colorIndex = json.IndexOf("\"color\":", labelIndex, StringComparison.Ordinal);
if (colorIndex != -1)
{
int vStart = json.IndexOf('\"', colorIndex + 8) + 1; // "color": => +8
if (vStart > 0)
{
int vEnd = json.IndexOf('\"', vStart);
if (vEnd > vStart) label.color = json.Substring(vStart, vEnd - vStart);
}
}
// description (may contain escaped quotes)
int descIndex = json.IndexOf("\"description\":", labelIndex, StringComparison.Ordinal);
if (descIndex != -1)
{
int qStart = json.IndexOf('\"', descIndex + 14) + 1; // "description": => +14
if (qStart > 0)
{
int i = qStart;
for (; i < json.Length; i++)
if (json[i] == '\"' && json[i - 1] != '\\') break;
if (i > qStart)
{
label.description = json.Substring(qStart, i - qStart).Replace("\\\"", "\"").Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\t", "\t");
// strip HTML tags for now
label.description = System.Text.RegularExpressions.Regex.Replace(label.description, "", string.Empty);
}
}
}
return label;
}
public static string ParseHashCodeFromURL(string url)
{
// https://beta.unity3d.com/download/330fbefc18b7/download.html#6000.1.0a8 > 330fbefc18b7
int hashStart = url.IndexOf("download/") + 9;
int hashEnd = url.IndexOf("/download.html", hashStart);
return url.Substring(hashStart, hashEnd - hashStart);
}
internal static void OpenReleasesApiForVersion(string version)
{
string url = $"https://services.api.unity.com/unity/editor/release/v1/releases?limit=1&version={version}";
LaunchBrowser(url);
}
private static void LaunchBrowser(string url)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
}
catch (Exception ex)
{
MessageBox.Show("Failed to open URL: " + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
internal static bool IsVersionAtLeast(string version, string v)
{
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(v)) return false;
try
{
var versionParts = version.Split('.');
var vParts = v.Split('.');
int versionMajor = 0;
int vMajor = 0;
int versionMinor = 0;
int vMinor = 0;
if (versionParts.Length > 0)
{
int i = 0;
while (i < versionParts[0].Length && char.IsDigit(versionParts[0][i])) i++;
int.TryParse(versionParts[0].Substring(0, i), out versionMajor);
}
if (vParts.Length > 0)
{
int i = 0;
while (i < vParts[0].Length && char.IsDigit(vParts[0][i])) i++;
int.TryParse(vParts[0].Substring(0, i), out vMajor);
}
if (versionMajor != vMajor)
{
return versionMajor > vMajor;
}
if (versionParts.Length > 1)
{
int i = 0;
while (i < versionParts[1].Length && char.IsDigit(versionParts[1][i])) i++;
int.TryParse(versionParts[1].Substring(0, i), out versionMinor);
}
if (vParts.Length > 1)
{
int i = 0;
while (i < vParts[1].Length && char.IsDigit(vParts[1][i])) i++;
int.TryParse(vParts[1].Substring(0, i), out vMinor);
}
return versionMinor >= vMinor;
}
catch
{
return false;
}
}
public static string GetBaseVersion(string version)
{
// e.g. 2020.3.15f1 -> 2020.3
var parts = version.Split('.');
if (parts.Length >= 2)
{
return parts[0] + "." + parts[1];
}
return version;
}
public static string SplitTextToRows(string description, int rows)
{
if (rows < 2) return description;
if (string.IsNullOrEmpty(description)) return description;
int len = description.Length;
if (len