( /* MAXPKG Installer Bridge for Autodesk 3ds Max ------------------------------------------------------------ Product: MAXPKG File: install.ms Purpose: Starts the installed MaxPkg Package Manager, installs it when the user explicitly allows it, and asks MaxPkg to install the embedded package GUID. Developer: MAXPKG / 3DGROUND Website: https://maxpkg.dev Author: Vasyl Lukianenko Contact: support.maxpkg@gmail.com Copyright: Copyright (c) MAXPKG / 3DGROUND. All rights reserved. License: This installer bridge is provided only for use with the official MAXPKG service. Unauthorized copying, modification, redistribution, reverse engineering, or use with third-party installer endpoints is prohibited. Security: Package metadata is embedded by the official maxpkg website before the script is downloaded. This bridge installs only the official MaxPkg Runtime from the maxpkg-dev/runtime repository after explicit user confirmation. Disclaimer: This software is provided "as is", without warranty of any kind. Use at your own risk. */ global MaxPkg global MaxPkgRuntimeManualDownloadUrl global MaxPkgRuntimeReleaseUrl global MaxPkgRuntimeManualInstallError local EMBEDDED_PACKAGE = "window-placer" local EMBEDDED_GUID = "87faf4ec-959d-41f3-984b-c3205383b4ed" local EMBEDDED_API_ORIGIN = "https://maxpkg.dev" local RUNTIME_MZP_URL = "https://github.com/maxpkg-dev/MaxPkg-Runtime-Manager/releases/latest/download/MaxPkg-Runtime-Manager.mzp" local RUNTIME_RELEASE_URL = "https://github.com/maxpkg-dev/MaxPkg-Runtime-Manager/releases/latest/" fn trimString s = ( if (s == undefined) do return "" local result = s as string while (result.count > 0 and ( substring result 1 1 == " " or substring result 1 1 == "\t" or substring result 1 1 == "\n" or substring result 1 1 == "\r" )) do ( result = substring result 2 (result.count - 1) ) while (result.count > 0 and ( substring result result.count 1 == " " or substring result result.count 1 == "\t" or substring result result.count 1 == "\n" or substring result result.count 1 == "\r" )) do ( result = substring result 1 (result.count - 1) ) return result ) fn isTemplateValue s = ( if (s == undefined) do return true return (findString (s as string) "{{{") != undefined ) fn getEmbeddedValue s = ( if (s == undefined) do return undefined local value = trimString (s as string) if (value == "" or isTemplateValue value) do return undefined return value ) fn ensureSlash folderPath = ( if (folderPath == undefined or folderPath == "") do return "" local normalizedPath = substituteString (folderPath as string) "/" @"\" if ((substring normalizedPath normalizedPath.count 1) == @"\") do return normalizedPath return normalizedPath + @"\" ) fn isSafeGuid s = ( if (s == undefined or s.count != 36) do return false local allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" for i in 1 to s.count do ( local ch = substring s i 1 if (findString allowed ch) == undefined do ( return false ) ) return true ) fn isSafePackageId s = ( if (s == undefined or s.count == 0) do return false local allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_/." for i in 1 to s.count do ( local ch = substring s i 1 if (findString allowed ch) == undefined do ( return false ) ) return true ) fn isSafeApiOrigin s = ( if (s == undefined or s.count == 0) do return false local lowered = toLower s return ( substring lowered 1 8 == "https://" or substring lowered 1 7 == "http://" ) ) fn trimTrailingSlash s = ( if (s == undefined or s.count == 0) do return "" if (substring s s.count 1 == "/") then ( return substring s 1 (s.count - 1) ) return s ) fn configureTls12 = ( try ( local servicePointManagerClass = dotNetClass "System.Net.ServicePointManager" local securityProtocolClass = dotNetClass "System.Net.SecurityProtocolType" servicePointManagerClass.SecurityProtocol = securityProtocolClass.Tls12 return true ) catch ( -- Ignore TLS setup errors on older systems. return false ) ) fn configureRuntime apiOrigin = ( if (MaxPkg == undefined) do return false if (not (isProperty MaxPkg #settings)) do return true if (MaxPkg.settings == undefined) do return true try ( if (isProperty MaxPkg.settings #apiEndpoint) do ( MaxPkg.settings.apiEndpoint = apiOrigin + "/api/runtime/v1/" ) if (isProperty MaxPkg #saveSettings) do MaxPkg.saveSettings() return true ) catch ( format "MaxPkg Runtime settings could not be updated: %\n" (getCurrentException()) return false ) ) fn maxPkgCanInstall = ( return MaxPkg != undefined and isProperty MaxPkg #install ) fn maxPkgIsStarted = ( if (not (maxPkgCanInstall())) do return false try ( if (isProperty MaxPkg #isStarted) do return MaxPkg.isStarted() ) catch ( return false ) return true ) fn ensureExistingRuntimeReady = ( if (not (maxPkgCanInstall())) do return false if (maxPkgIsStarted()) do return true try ( if (isProperty MaxPkg #start) do MaxPkg.start() ) catch ( format "MaxPkg Runtime could not be started: %\n" (getCurrentException()) ) return maxPkgIsStarted() ) fn isDevelopmentOrigin apiOrigin = ( if (apiOrigin == undefined) do return false local lowered = toLower (apiOrigin as string) return ( findString lowered "dev.maxpkg.dev" != undefined or findString lowered "localhost" != undefined or findString lowered "127.0.0.1" != undefined or findString lowered "192.168." != undefined ) ) fn prepareWebClient = ( configureTls12() local wc = dotNetObject "System.Net.WebClient" wc.Headers.Add "User-Agent" "MAXPKG-3dsMax-Installer" return wc ) fn openExternalUrl url = ( try ( shellLaunch url "" return true ) catch ( try ( local processClass = dotNetClass "System.Diagnostics.Process" processClass.Start url return true ) catch ( messageBox ("Could not open the download URL automatically.\n\n" + (url as string)) title:"MaxPkg" return false ) ) ) fn showRuntimeManualDownload errorMessage runtimeMzpUrl runtimeReleaseUrl = ( MaxPkgRuntimeManualDownloadUrl = runtimeMzpUrl MaxPkgRuntimeReleaseUrl = runtimeReleaseUrl MaxPkgRuntimeManualInstallError = errorMessage try ( destroyDialog MaxPkgRuntimeManualDownloadRollout ) catch ( ) rollout MaxPkgRuntimeManualDownloadRollout "MaxPkg Runtime installation" width: 540 height: 290 ( label statusLabel "MaxPkg Package Manager could not be installed automatically." pos: [20, 18] width: 480 height: 20 label detailLabel "Download the Runtime MZP and drag it into a 3ds Max viewport." pos: [20, 48] width: 500 height: 20 hyperlink downloadLink "Download MaxPkg Runtime Manager MZP" address: "https://github.com/maxpkg-dev/MaxPkg-Runtime-Manager/releases/latest/download/MaxPkg-Runtime-Manager.mzp" pos: [20, 78] width: 500 height: 22 color: (color 120 155 255) hoverColor: (color 165 195 255) visitedColor: (color 120 155 255) editText urlText "" pos: [20, 108] width: 500 height: 22 readOnly: true groupBox errorGroup "What happened" pos: [20, 142] width: 500 height: 62 label errorText "" pos: [32, 164] width: 476 height: 34 button downloadButton "Download MZP" pos: [142, 230] width: 120 height: 28 button releasesButton "Open releases" pos: [272, 230] width: 120 height: 28 button closeButton "Close" pos: [402, 230] width: 120 height: 28 on MaxPkgRuntimeManualDownloadRollout open do ( if (MaxPkgRuntimeManualDownloadUrl != undefined and MaxPkgRuntimeManualDownloadUrl != "") do ( downloadLink.address = MaxPkgRuntimeManualDownloadUrl urlText.text = MaxPkgRuntimeManualDownloadUrl ) if (MaxPkgRuntimeManualInstallError != undefined and MaxPkgRuntimeManualInstallError != "") do ( errorText.text = MaxPkgRuntimeManualInstallError ) ) on downloadButton pressed do ( if (MaxPkgRuntimeManualDownloadUrl != undefined and MaxPkgRuntimeManualDownloadUrl != "") do ( openExternalUrl MaxPkgRuntimeManualDownloadUrl ) ) on releasesButton pressed do ( if (MaxPkgRuntimeReleaseUrl != undefined and MaxPkgRuntimeReleaseUrl != "") do ( openExternalUrl MaxPkgRuntimeReleaseUrl ) ) on closeButton pressed do ( destroyDialog MaxPkgRuntimeManualDownloadRollout ) ) createDialog MaxPkgRuntimeManualDownloadRollout style: #(#style_titlebar, #style_sysmenu, #style_toolwindow) ) fn deleteFileSafe filePath = ( if (filePath == undefined or filePath == "") do return true try ( if (doesFileExist filePath) do ( local fileInfo = dotNetObject "System.IO.FileInfo" filePath if (fileInfo.IsReadOnly) do fileInfo.IsReadOnly = false deleteFile filePath ) return true ) catch ( format "MaxPkg temporary file cleanup warning: %\n" (getCurrentException()) return false ) ) fn fileIsNonEmpty filePath = ( if (filePath == undefined or not (doesFileExist filePath)) do return false try ( local fileInfo = dotNetObject "System.IO.FileInfo" filePath return fileInfo.Length > 0 ) catch ( return false ) ) fn downloadFileWithWebClient url destinationPath = ( try ( local wc = prepareWebClient() wc.DownloadFile url destinationPath return fileIsNonEmpty destinationPath ) catch ( deleteFileSafe destinationPath return false ) ) fn downloadFileWithCurl url destinationPath = ( try ( deleteFileSafe destinationPath local startInfo = dotNetObject "System.Diagnostics.ProcessStartInfo" startInfo.FileName = "curl.exe" startInfo.Arguments = "--location --fail --silent --show-error --output \"" + destinationPath + "\" \"" + url + "\"" startInfo.UseShellExecute = false startInfo.CreateNoWindow = true local process = dotNetObject "System.Diagnostics.Process" process.StartInfo = startInfo if (not (process.Start())) do return false process.WaitForExit() local succeeded = process.ExitCode == 0 and fileIsNonEmpty destinationPath process.Dispose() if (not succeeded) do deleteFileSafe destinationPath return succeeded ) catch ( deleteFileSafe destinationPath return false ) ) fn downloadRuntimeMzp url destinationPath = ( if (downloadFileWithWebClient url destinationPath) do return true return downloadFileWithCurl url destinationPath ) fn createRuntimeTempPath = ( local tempFolder = ensureSlash (getDir #temp) local uniquePart = ((timestamp()) as string) + "-" + ((random 100000 999999) as string) return tempFolder + "MaxPkg-Runtime-Manager-" + uniquePart + ".mzp" ) fn installRuntimeFromRelease apiOrigin = ( local runtimeMzpPath = createRuntimeTempPath() local runtimeMzpUrl = getEmbeddedValue RUNTIME_MZP_URL local runtimeReleaseUrl = getEmbeddedValue RUNTIME_RELEASE_URL local progressIsOpen = false progressStart "Installing MaxPkg Package Manager..." progressIsOpen = true try ( if (runtimeMzpUrl == undefined or runtimeMzpUrl == "") do throw "Runtime MZP download URL is missing." progressUpdate 15 deleteFileSafe runtimeMzpPath progressUpdate 30 if (not (downloadRuntimeMzp runtimeMzpUrl runtimeMzpPath)) do throw "Runtime MZP download failed with WebClient and curl.exe." progressUpdate 70 fileIn runtimeMzpPath progressUpdate 90 if (not (ensureExistingRuntimeReady())) do throw "MaxPkg Runtime was installed, but did not start." progressUpdate 100 progressEnd() progressIsOpen = false deleteFileSafe runtimeMzpPath return true ) catch ( local installError = getCurrentException() if (progressIsOpen) do progressEnd() deleteFileSafe runtimeMzpPath local publicError = "The automatic Runtime installation could not be completed." if (isDevelopmentOrigin apiOrigin) do publicError = installError showRuntimeManualDownload publicError runtimeMzpUrl runtimeReleaseUrl return false ) ) fn ensureRuntimeInstalled apiOrigin = ( if (ensureExistingRuntimeReady()) do return true local shouldInstall = queryBox ( "MaxPkg Package Manager is not installed.\n\n" + "It is required to install this package.\n\n" + "Install it now?" ) title:"MaxPkg" if (not shouldInstall) do ( messageBox "Nothing was installed.\n\nYou declined to install MaxPkg Package Manager." title:"MaxPkg" return false ) return installRuntimeFromRelease apiOrigin ) fn installStatusIsSuccess status = ( if (status == true) do return true if (status == false or status == undefined) do return false try ( if (isProperty status #success) do return status.success == true if (isProperty status #isSuccess) do return status.isSuccess == true if (isProperty status #ok) do return status.ok == true ) catch ( ) return false ) fn installStatusMessage status fallbackMessage = ( if (fallbackMessage == undefined) do fallbackMessage = "" try ( if (status != undefined and isProperty status #message and status.message != undefined and status.message != "") do ( return status.message as string ) if (status != undefined and isProperty status #error and status.error != undefined and status.error != "") do ( return status.error as string ) ) catch ( ) return fallbackMessage ) fn showPackageInstallResult packageId status = ( if (installStatusIsSuccess status) then ( local successMessage = installStatusMessage status ("Package installed successfully.\n\n" + packageId) if(::MaxPkg == undefined) do messageBox successMessage title:"MaxPkg" return true ) local failureMessage = installStatusMessage status ( "Package installation failed.\n\n" + "Open MaxPkg Package Manager for details, or try again from the package page.\n\n" + packageId ) if(::MaxPkg == undefined) do messageBox failureMessage title:"MaxPkg" return false ) try ( configureTls12() local packageId = getEmbeddedValue EMBEDDED_PACKAGE local packageGuid = getEmbeddedValue EMBEDDED_GUID local apiOrigin = getEmbeddedValue EMBEDDED_API_ORIGIN if (apiOrigin != undefined) do apiOrigin = trimTrailingSlash apiOrigin if (packageId == undefined or packageId == "") then ( messageBox "MAXPKG package data is missing.\n\nOpen the package page and drag the installer from the Install dialog." title:"MaxPkg" ) else if (not isSafePackageId packageId) then ( messageBox ("Invalid MAXPKG package id:\n\n" + (packageId as string)) title:"MaxPkg" ) else if (packageGuid == undefined or not (isSafeGuid packageGuid)) then ( messageBox "MAXPKG package GUID is missing or invalid.\n\nOpen the package page and drag the installer from the Install dialog." title:"MaxPkg" ) else if (not isSafeApiOrigin apiOrigin) then ( messageBox "MAXPKG API origin is missing or invalid.\n\nOpen the package page and drag the installer from the Install dialog." title:"MaxPkg" ) else if (ensureRuntimeInstalled apiOrigin) then ( configureRuntime apiOrigin if (maxPkgCanInstall()) then ( local installStatus = MaxPkg.install packageGuid showPackageInstallResult packageId installStatus ) else ( messageBox "MaxPkg Package Manager is installed, but its install command is unavailable." title:"MaxPkg" ) ) ) catch ( local failureMessage = "MAXPKG installation could not be completed.\n\nTry again, or install MaxPkg Runtime Manager manually from the latest GitHub release." local currentOrigin = getEmbeddedValue EMBEDDED_API_ORIGIN if (isDevelopmentOrigin currentOrigin) do ( failureMessage += "\n\nTechnical details:\n" + getCurrentException() ) messageBox failureMessage title:"MaxPkg" showRuntimeManualDownload failureMessage (getEmbeddedValue RUNTIME_MZP_URL) (getEmbeddedValue RUNTIME_RELEASE_URL) ) )