├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── MXSPyCOM.code-workspace ├── MXSPyCOM.csproj ├── Program.cs ├── README.md ├── hello_world.ms ├── hello_world.py ├── images ├── notepad++_menu_post.png ├── notepad++_menu_pre.png ├── notepad++_run_dialog.png ├── notepad++_shortcut_dialog.png ├── pycharm_create_tool_dialog.png ├── pycharm_menu_post.png ├── pycharm_settings_dialog.png ├── sublime_key_bindings_dialog.png ├── sublime_menu_key_bindings.png ├── sublime_menu_package_control.png ├── ue_configure_tools_dialog_options_tab.png ├── ue_configure_tools_dialog_output_tab.png ├── ue_external_tools_dialog.png ├── ue_ribbon_post.png ├── ue_ribbon_pre.png ├── vs_external_tools_dialog.png ├── vs_menu_post.png ├── vs_menu_pre.png ├── wing_prefs_dialog_command_create.png ├── wing_prefs_dialog_post.png └── wing_prefs_dialog_pre.png ├── initialize_COM_server.ms └── scripts └── publish.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | .mypy_cache/ 13 | bin/ 14 | obj/ 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | [Xx]64/ 22 | [Xx]86/ 23 | [Bb]uild/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | artifacts/ 49 | 50 | *_i.c 51 | *_p.c 52 | *_i.h 53 | *.ilk 54 | *.meta 55 | *.obj 56 | *.pch 57 | *.pdb 58 | *.pgc 59 | *.pgd 60 | *.rsp 61 | *.sbr 62 | *.tlb 63 | *.tli 64 | *.tlh 65 | *.tmp 66 | *.tmp_proj 67 | *.log 68 | *.vspscc 69 | *.vssscc 70 | .builds 71 | *.pidb 72 | *.svclog 73 | *.scc 74 | 75 | # Chutzpah Test files 76 | _Chutzpah* 77 | 78 | # Visual C++ cache files 79 | ipch/ 80 | *.aps 81 | *.ncb 82 | *.opendb 83 | *.opensdf 84 | *.sdf 85 | *.cachefile 86 | *.VC.db 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.[Pp]ublish.xml 144 | *.azurePubxml 145 | 146 | # TODO: Un-comment the next line if you do not want to checkin 147 | # your web deploy settings because they may include unencrypted 148 | # passwords 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # NuGet Packages 153 | *.nupkg 154 | # The packages folder can be ignored because of Package Restore 155 | **/packages/* 156 | # except build/, which is used as an MSBuild target. 157 | !**/packages/build/ 158 | # Uncomment if necessary however generally it will be regenerated when needed 159 | #!**/packages/repositories.config 160 | # NuGet v3's project.json files produces more ignoreable files 161 | *.nuget.props 162 | *.nuget.targets 163 | 164 | # Microsoft Azure Build Output 165 | csx/ 166 | *.build.csdef 167 | 168 | # Microsoft Azure Emulator 169 | ecf/ 170 | rcf/ 171 | 172 | # Microsoft Azure ApplicationInsights config file 173 | ApplicationInsights.config 174 | 175 | # Windows Store app package directory 176 | AppPackages/ 177 | BundleArtifacts/ 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | [Ss]tyle[Cc]op.* 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # RIA/Silverlight projects 198 | Generated_Code/ 199 | 200 | # Backup & report files from converting an old project file 201 | # to a newer Visual Studio version. Backup files are not needed, 202 | # because we have git ;-) 203 | _UpgradeReport_Files/ 204 | Backup*/ 205 | UpgradeLog*.XML 206 | UpgradeLog*.htm 207 | 208 | # SQL Server files 209 | *.mdf 210 | *.ldf 211 | 212 | # Business Intelligence projects 213 | *.rdl.data 214 | *.bim.layout 215 | *.bim_*.settings 216 | 217 | # Microsoft Fakes 218 | FakesAssemblies/ 219 | 220 | # GhostDoc plugin setting file 221 | *.GhostDoc.xml 222 | 223 | # Node.js Tools for Visual Studio 224 | .ntvs_analysis.dat 225 | 226 | # Visual Studio 6 build log 227 | *.plg 228 | 229 | # Visual Studio 6 workspace options file 230 | *.opt 231 | 232 | # Visual Studio LightSwitch build output 233 | **/*.HTMLClient/GeneratedArtifacts 234 | **/*.DesktopClient/GeneratedArtifacts 235 | **/*.DesktopClient/ModelManifest.xml 236 | **/*.Server/GeneratedArtifacts 237 | **/*.Server/ModelManifest.xml 238 | _Pvt_Extensions 239 | 240 | # LightSwitch generated files 241 | GeneratedArtifacts/ 242 | ModelManifest.xml 243 | 244 | # Paket dependency manager 245 | .paket/paket.exe 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # Documentation images 251 | images/*.* 252 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/MXSPyCOM.exe", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.mypy_cache": true, 4 | "**/.vs": true, 5 | "**/bin": true, 6 | "**/obj": true, 7 | } 8 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build Debug", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/MXSPyCOM.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | }, 14 | { 15 | "label": "Build Release", 16 | "command": "dotnet", 17 | "type": "process", 18 | "args": [ 19 | "build", 20 | "${workspaceFolder}/MXSPyCOM.csproj", 21 | "-c", 22 | "release" 23 | ], 24 | "problemMatcher": "$msCompile", 25 | "group": { 26 | "kind": "build", 27 | "isDefault": true 28 | } 29 | }, 30 | { 31 | "label": "Publish", 32 | "type": "shell", 33 | "command": "scripts\\publish.ps1", 34 | "problemMatcher": "$msCompile" 35 | }, 36 | { 37 | "label": "Watch", 38 | "command": "dotnet", 39 | "type": "process", 40 | "args": [ 41 | "watch", 42 | "run", 43 | "${workspaceFolder}/MXSPyCOM.csproj" 44 | ], 45 | "problemMatcher": "$msCompile" 46 | }, 47 | { 48 | "label": "Execute Script in 3ds Max (Debug)", 49 | "type": "shell", 50 | "command": "${workspaceFolder}/bin/Debug/net6.0-windows/win-x64/MXSPyCOM.exe", 51 | "args": [ 52 | "-s", 53 | "${file}" 54 | ], 55 | "presentation": { 56 | "echo": false, 57 | "reveal": "never", 58 | "focus": false, 59 | "panel": "dedicated" 60 | }, 61 | "problemMatcher": [] 62 | }, 63 | { 64 | "label": "Execute Script in 3ds Max", 65 | "type": "shell", 66 | "command": "${workspaceFolder}/bin/Release/net6.0-windows/win-x64/publish/MXSPyCOM.exe", 67 | "args": [ 68 | "-s", 69 | "${file}" 70 | ], 71 | "presentation": { 72 | "echo": false, 73 | "reveal": "never", 74 | "focus": false, 75 | "panel": "dedicated" 76 | }, 77 | "problemMatcher": [] 78 | } 79 | ] 80 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jeff Hanna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MXSPyCOM.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /MXSPyCOM.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0-windows 6 | false 7 | true 8 | win-x64 9 | true 10 | false 11 | MXSPyCOM.Program 12 | true 13 | 1.14 14 | Jeff Hanna 15 | Technical Artists Forum, Inc 16 | Copyright © 2022, Technical Artists Forum, Inc. All rights reserved. 17 | http://www.tech-artists.org 18 | https://github.com/techartorg/MXSPyCOM 19 | A modern version of MXSCOM, to allow for editing & execution of 3ds Max MaxScript and Python files from external code editors. 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Diagnostics; 4 | using System.Reflection; 5 | using System.Windows.Forms; 6 | 7 | using Westwind.Utilities; 8 | 9 | 10 | namespace MXSPyCOM 11 | { 12 | class Program 13 | { 14 | /// A modern version of MXSCOM, to allow for editing & execution of 3ds Max MaxScript and Python files from external code editors. 15 | /// In 2005 Simon Feltman released the first MXSCOM, a small Visual Basic 6 application that took commands and sent them to 16 | /// Autodesk's 3ds Max's internal COM server. This allowed users to choose their own external code editor for editing MaxScript 17 | /// and to be able to have their MaxScript code execute in 3ds Max by way of having the code editor utilize MXSCOM to send the file 18 | /// into 3ds Max and have it executed. Modern versions of Windows can not use Simon Feltman's old MXSCOM.exe program due 19 | /// to it being ActiveX based. 20 | /// 21 | /// MXSPyCOM is a C# based replacement for MXSCOM. It offers the same functionality as MXSCOM but can run on modern versions of Windows. 22 | /// It also supports editing of Python files and having them execute in versions of 3ds Max, starting with 3ds Max 2015, that support 23 | /// Python scripts. 24 | /// 25 | /// **Arguments:** 26 | /// 27 | /// None 28 | /// 29 | /// **Keyword Arguments:** 30 | /// 31 | /// None 32 | /// 33 | /// **TODO** 34 | /// 35 | /// :Add the ability to start 3ds Max if it is not found as a running process. 36 | /// 37 | /// **Author:** 38 | /// 39 | /// Jeff Hanna, jeff@techart.online, July 9, 2016 40 | 41 | const string USAGE_INFO = "\nType \"MXSPyCOM\" for usage info."; 42 | 43 | 44 | static void execute_max_commands(string[] args, string filepath) 45 | { 46 | /// Parses the command line arguments and calls the corresponding command on Max's COM server with the provied filepath. 47 | /// 48 | /// **Arguments:** 49 | /// 50 | /// :``args``: `string[]`: The command line arguments sent to MXSPyCOM 51 | /// :``filepath: `string`: A full absolute filepath to a MaxScript (.ms) or Python (.py) file. 52 | /// 53 | /// **Keyword Arguments:** 54 | /// 55 | /// None 56 | /// 57 | /// **Returns:** 58 | /// 59 | /// None 60 | /// 61 | /// **TODO:** 62 | /// 63 | /// Should support for Max's COM server execute() function be added? It doesn't act on files, but on strings of MaxScript commands. 64 | /// It doesn't seem necessary for this tool, honestly. 65 | /// 66 | /// **Author:** 67 | /// 68 | /// Jeff Hanna, jeff@techart.online, July 11, 2016 69 | 70 | bool max_running = is_process_running("3dsmax"); 71 | if (max_running) 72 | { 73 | if (args.Length == 1) 74 | { 75 | string msg = String.Format("No options provided.", USAGE_INFO); 76 | show_message(msg); 77 | } 78 | else 79 | { 80 | string prog_id = "Max.Application"; 81 | Type com_type = Type.GetTypeFromProgID(prog_id); 82 | object com_obj = Activator.CreateInstance(com_type); 83 | 84 | string ext = System.IO.Path.GetExtension(filepath).ToLower(); 85 | 86 | foreach (string arg in args) 87 | { 88 | switch (arg.ToLower()) 89 | { 90 | case "-f": 91 | if (ext == ".py") 92 | { 93 | filepath = make_python_wrapper(filepath); 94 | } 95 | 96 | try 97 | { 98 | com_obj.GetType().InvokeMember("filein", 99 | ReflectionUtils.MemberAccess | BindingFlags.InvokeMethod, 100 | null, 101 | com_obj, 102 | new object[] {filepath}); 103 | } 104 | catch (System.Reflection.TargetInvocationException) { } 105 | break; 106 | 107 | case "-s": 108 | try 109 | { 110 | filepath = mxs_try_catch_errors_cmd(filepath); 111 | com_obj.GetType().InvokeMember("execute", 112 | ReflectionUtils.MemberAccess | BindingFlags.InvokeMethod, 113 | null, 114 | com_obj, 115 | new object[] {filepath}); 116 | } 117 | catch (System.Reflection.TargetInvocationException) { } 118 | break; 119 | 120 | case "-e": 121 | try 122 | { 123 | com_obj.GetType().InvokeMember("edit", 124 | ReflectionUtils.MemberAccess | BindingFlags.InvokeMethod, 125 | null, 126 | com_obj, 127 | new object[] {filepath}); 128 | } 129 | catch (System.Reflection.TargetInvocationException) { } 130 | break; 131 | 132 | case "-c": 133 | if (ext == ".ms") 134 | { 135 | try 136 | { 137 | com_obj.GetType().InvokeMember("encryptscript", 138 | ReflectionUtils.MemberAccess | BindingFlags.InvokeMethod, 139 | null, 140 | com_obj, 141 | new object[] {filepath}); 142 | } 143 | catch (System.Reflection.TargetInvocationException) { } 144 | } 145 | else 146 | { 147 | string msg = String.Format("Only MaxScript files can be encrypted. {0}", USAGE_INFO); 148 | show_message(msg); 149 | } 150 | break; 151 | 152 | default: 153 | return; 154 | } 155 | } 156 | } 157 | } 158 | else 159 | { 160 | string msg = "3ds Max is not currently running."; 161 | show_message(msg); 162 | } 163 | 164 | return; 165 | } 166 | 167 | 168 | static bool is_process_running(string process_name) 169 | { 170 | /// Determines if a named process is currently running. 171 | /// 172 | /// **Arguments:** 173 | /// 174 | /// :``process_name``: `string` The name of the process (minus the .exe extension) to check 175 | /// 176 | /// **Keyword Arguments:** 177 | /// 178 | /// None 179 | /// 180 | /// **Returns:** 181 | /// 182 | /// :`bool` 183 | /// 184 | /// **Author:** 185 | /// 186 | /// Jeff Hanna, jeff@techart.online, July 9, 2016 187 | 188 | Process[] pname = Process.GetProcessesByName(process_name); 189 | if (pname.Length > 0) 190 | { 191 | return true; 192 | } 193 | 194 | return false; 195 | } 196 | 197 | 198 | static string make_python_import_reload_wrapper(string python_filepath) 199 | { 200 | /// It is not possible to directly execute Python files in 3ds Max via calling filein() on the COM server. 201 | /// Luckily MaxScript supports Python.ExecuteFile(filepath). This function takes the provided Python file 202 | /// and wraps it a Python.ExecuteFile() command within a MaxScript file that is saved to the user's %TEMP% folder. 203 | /// That temporary MaxScript file is what is sent to 3ds Max's COM server. 204 | /// 205 | /// **Arguments:** 206 | /// 207 | /// :``python_filepath``: `string` A full absolute filepath to a Python (.py) file. 208 | /// 209 | /// **Keyword Arguments:** 210 | /// 211 | /// None 212 | /// 213 | /// **Returns:** 214 | /// 215 | /// :``reload_wrapper_filepath``: `string` 216 | /// 217 | /// **Author:** 218 | /// 219 | /// Jeff Hanna, jeff@techart.online, November 22, 2019 220 | 221 | string mod_name = Path.GetFileNameWithoutExtension(python_filepath); 222 | string reload_cmd = String.Format("import sys\nif sys.version_info.major < 3:\n\timport imp as importlib\nelse:\n\timport importlib\n\t\ntry:\n\timportlib.reload({0})\nexcept:\n\tpass", mod_name); 223 | var reload_wrapper_filepath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "import_reload.py"); 224 | reload_wrapper_filepath = reload_wrapper_filepath.Replace( "\\", "\\\\"); 225 | System.IO.File.WriteAllText(reload_wrapper_filepath, reload_cmd); 226 | 227 | return reload_wrapper_filepath; 228 | } 229 | 230 | 231 | static string make_python_wrapper(string python_filepath) 232 | { 233 | /// For the python file being executed in 3ds Max this script writes a Maxscript wrapper file 234 | /// that can be called to reimport that Python module so that the in-memory version is updated with 235 | /// any changes made between script executions. 236 | /// 237 | /// **Arguments:** 238 | /// 239 | /// :``python_filepath``: `string` A full absolute filepath to a Python (.py) file. 240 | /// 241 | /// **Keyword Arguments:** 242 | /// 243 | /// None 244 | /// 245 | /// **Returns:** 246 | /// 247 | /// :``wrapper_filepath``: `string` 248 | /// 249 | /// **Author:** 250 | /// 251 | /// Jeff Hanna, jeff@techart.online, July 9, 2016 252 | 253 | string reload_filepath = make_python_import_reload_wrapper( python_filepath ); 254 | string cmd = String.Format("python.ExecuteFile(\"{0}\");python.ExecuteFile(\"{1}\")", reload_filepath, python_filepath); 255 | var wrapper_filepath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "maxscript_python_wrapper.ms"); 256 | System.IO.File.WriteAllText(wrapper_filepath, cmd); 257 | 258 | return wrapper_filepath; 259 | } 260 | 261 | 262 | static string mxs_try_catch_errors_cmd(string filepath) 263 | { 264 | /// Wraps the given MAXScript command arg in MAXScript code that when run in 3ds Max will catch 265 | /// errors using a MAXScript try() catch() and print a 'homemade' minimally useful log message 266 | /// to the MAXScript Listener instead of the usual proper one. 267 | /// 268 | /// **Arguments:** 269 | /// 270 | /// :``command: `string` A fumaxscript command to wrap 271 | /// :``log_filepath``: `string` The filepath to use in the MAXScript Listerner log message 272 | /// 273 | /// **Keyword Arguments:** 274 | /// 275 | /// None 276 | /// 277 | /// **Returns:** 278 | /// 279 | /// :``cmd``: `string` 280 | /// 281 | /// **Author:** 282 | /// 283 | /// Gary Tyler, mail@garytyler.com, April 12, 2018 284 | 285 | string ext = System.IO.Path.GetExtension(filepath).ToLower(); 286 | 287 | string location; 288 | string run_cmd; 289 | 290 | if (ext == ".py") 291 | { 292 | /// For python files, use passed filepath for location msg, no pos or line available 293 | location = String.Format("\"Error; filename: {0}\"", filepath); 294 | 295 | /// Pass thru python.ExecuteFile() 296 | string reload_filepath = make_python_import_reload_wrapper( filepath ); 297 | run_cmd = String.Format("python.ExecuteFile(\"{0}\");python.ExecuteFile(\"{1}\")", reload_filepath, filepath); 298 | } 299 | else 300 | { 301 | /// For maxscript files, use provided commands for location msg 302 | string loc_file = "\" filename: \" + (getErrorSourceFileName() as string)"; 303 | string loc_pos = "\"; position: \" + ((getErrorSourceFileOffset() as integer) as string)"; 304 | string loc_line = "\"; line: \" + ((getErrorSourceFileLine() as integer) as string) + \"\n\""; 305 | string callstack_line = "\"callstack: \n\" + (cs as string)"; 306 | location = String.Format("\"Error;\" + {0} + {1} + {2} + {3}", loc_file, loc_pos, loc_line, callstack_line); 307 | 308 | /// Pass thru filein() 309 | run_cmd = String.Format("filein(@\"{0}\")", filepath); 310 | } 311 | 312 | string exception_array = "(filterString (getCurrentException()) \"\n\")"; 313 | string exception_msg = String.Format("(for i in #({0}) + {1} do setListenerSelText (\"\n\" + i))", location, exception_array); 314 | string cmd = String.Format("try({0}) catch(cs = \"\" as stringStream;stack to:cs;{1});setListenerSelText \"\n\"", run_cmd, exception_msg); 315 | return cmd; 316 | } 317 | 318 | 319 | static void show_message(string message, bool info = false) 320 | { 321 | /// Displays an error or informational dialog if the execution of MXSPyCOM encounters a problem. 322 | /// Also displays a standard help dialog if MXSPyCOM is called with no arguments or with the /? or /help arguments. 323 | /// 324 | /// **Arguments:** 325 | /// 326 | /// :``message``: `string` The message to display. 327 | /// 328 | /// **Keyword Arguments:** 329 | /// 330 | /// :``information``: `bool` Determines if the dialog should display with an informational or warning icon. 331 | /// 332 | /// **Returns:** 333 | /// 334 | /// None 335 | /// 336 | /// **Author:** 337 | /// 338 | /// Jeff Hanna, jeff@techart.online, July 9, 2016 339 | 340 | MessageBoxIcon icon = info == true ? MessageBoxIcon.Information : MessageBoxIcon.Error; 341 | 342 | if (message.ToLower() == "help") 343 | { 344 | message = @"Used to execute MaxScript and Python scripts in 3ds Max. 345 | 346 | Usage: 347 | MXSPyCOM.exe [options] 348 | 349 | Options: 350 | -f - Execute the script in 3ds Max. 351 | -s - Execute the script in 3ds Max with no error dialogs. 352 | -e - Edit the script in 3ds Max's internal script editor. 353 | -c - Encrypt the script. Only works with MaxScript files. 354 | 355 | Commands: 356 | - Full path to the script file to execute."; 357 | 358 | show_message(message, info: true); 359 | } 360 | 361 | MessageBox.Show(message, "MXSPyCOM", MessageBoxButtons.OK, icon); 362 | 363 | Environment.Exit(0); 364 | } 365 | 366 | 367 | static string get_script_filepath(string[] args) 368 | { 369 | /// Extracts the script filepath from the command line arguments. 370 | /// If no filepath is provied in args the user is notifed and the program exits. 371 | /// If the filepath provided contains a Python script that script is wrapped in 372 | /// a MaxScript wrapper, because 3ds Max's COM interface will not correctly parse 373 | /// a Python file, and the filepath to that temporary wrapper script is returned. 374 | /// 375 | /// **Arguments:** 376 | /// 377 | /// :``args``: `string[]` The arguments provided to MXSPyCOM 378 | /// 379 | /// **Keyword Arguments:** 380 | /// 381 | /// None 382 | /// 383 | /// **Returns:** 384 | /// 385 | /// :``filepath``: `string` A full absolute filepath to the script to execute in 3ds Max. 386 | /// 387 | /// **Author:** 388 | /// 389 | /// Jeff Hanna, jeff@techart.online, July 9, 2016 390 | 391 | string filepath = args[args.Length - 1]; 392 | if (filepath.StartsWith("-")) 393 | { 394 | string msg = String.Format("No script filepath provided. {0}", USAGE_INFO); 395 | show_message(msg); 396 | } 397 | else if (!System.IO.File.Exists(filepath)) 398 | { 399 | string msg = String.Format("The specified script file does not exist on disk. {0}", USAGE_INFO); 400 | show_message(msg); 401 | } 402 | 403 | filepath = filepath.Replace("\\", "\\\\"); 404 | return filepath; 405 | } 406 | 407 | 408 | static void Main(string[] args) 409 | { 410 | /// The main execution function of MXSPyCOM 411 | /// 412 | /// **Arguments:** 413 | /// 414 | /// :``args``: `string[]` The arguments provided to MXSPyCOM 415 | /// 416 | /// **Keyword Arguments:** 417 | /// 418 | /// None 419 | /// 420 | /// **Returns:** 421 | /// 422 | /// None 423 | /// 424 | /// **Author:** 425 | /// 426 | /// Jeff Hanna, jeff@techart.online, July 9, 2016 427 | 428 | if (args.Length == 0) 429 | { 430 | show_message("help"); 431 | 432 | // For testing 433 | //string filepath = @"d:\repos\mxspycom\hello_world.ms"; 434 | //string[] test_args = new string[2] {"-f", filepath}; 435 | //execute_max_commands(test_args, filepath); 436 | } 437 | else 438 | { 439 | string filepath = get_script_filepath(args); 440 | execute_max_commands(args, filepath); 441 | } 442 | 443 | Environment.Exit(0); 444 | } 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MXSPyCOM 2 | A modern version of MXSCOM, to allow for editing & execution of 3ds Max MaxScript and Python files from external code editors. 3 | 4 | In 2005 Simon Feltman released the first MXSCOM, a small Visual Basic 6 application that took commands and sent them to Autodesk's 5 | 3ds Max's internal COM server. This allowed users to choose their own external code editor for editing MaxScript and to be able to 6 | have their MaxScript code execute in 3ds Max by way of having the code editor utilize MXSCOM to send the file into 3ds Max and have it 7 | executed. Modern versions of Windows can not use Simon Feltman's old MXSCOM.exe program due to it being ActiveX based. 8 | 9 | MXSPyCOM is a C# based replacement for MXSCOM. It offers the same functionality as MXSCOM but can run on modern versions of Windows. 10 | It also supports editing of Python files and having them execute in versions of 3ds Max, starting with 3ds Max 2015, that support Python 11 | scripts. 12 | 13 | Instructions on how to install MXSPyCOM and how to configure most popular code editors to use it as an external tool can be found in [this project's Wiki](https://github.com/JeffHanna/MXSPyCOM/wiki). 14 | -------------------------------------------------------------------------------- /hello_world.ms: -------------------------------------------------------------------------------- 1 | ( 2 | messageBox "Hello World!" 3 | ) 4 | -------------------------------------------------------------------------------- /hello_world.py: -------------------------------------------------------------------------------- 1 | """ 2 | A "Hello Word!" example as a Qt based messagebox. 3 | This is used as a test to ensure MXSPyCOM is working correctly with 3ds Max. 4 | """ 5 | 6 | from pymxs import runtime # type: ignore 7 | 8 | try: 9 | import MaxPlus # type: ignore 10 | except ImportError: 11 | import shiboken2 # type: ignore 12 | MaxPlus = None 13 | 14 | from PySide2.QtWidgets import QMainWindow, QMessageBox, QWidget # type: ignore 15 | 16 | 17 | def main() -> None: 18 | if MaxPlus: 19 | main_window = MaxPlus.GetQMaxMainWindow() # type: QMainWindow 20 | else: 21 | main_window_widget = QWidget.find(int(runtime.windows.getMAXHWND())) # type: QWidget 22 | main_window = shiboken2.wrapInstance(shiboken2.getCppPointer(main_window_widget)[0], QMainWindow) # type: QMainWindow 23 | 24 | msg_box = QMessageBox(main_window) 25 | msg_box.setText('Hello World!') 26 | msg_box.show() 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /images/notepad++_menu_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/notepad++_menu_post.png -------------------------------------------------------------------------------- /images/notepad++_menu_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/notepad++_menu_pre.png -------------------------------------------------------------------------------- /images/notepad++_run_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/notepad++_run_dialog.png -------------------------------------------------------------------------------- /images/notepad++_shortcut_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/notepad++_shortcut_dialog.png -------------------------------------------------------------------------------- /images/pycharm_create_tool_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/pycharm_create_tool_dialog.png -------------------------------------------------------------------------------- /images/pycharm_menu_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/pycharm_menu_post.png -------------------------------------------------------------------------------- /images/pycharm_settings_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/pycharm_settings_dialog.png -------------------------------------------------------------------------------- /images/sublime_key_bindings_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/sublime_key_bindings_dialog.png -------------------------------------------------------------------------------- /images/sublime_menu_key_bindings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/sublime_menu_key_bindings.png -------------------------------------------------------------------------------- /images/sublime_menu_package_control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/sublime_menu_package_control.png -------------------------------------------------------------------------------- /images/ue_configure_tools_dialog_options_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/ue_configure_tools_dialog_options_tab.png -------------------------------------------------------------------------------- /images/ue_configure_tools_dialog_output_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/ue_configure_tools_dialog_output_tab.png -------------------------------------------------------------------------------- /images/ue_external_tools_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/ue_external_tools_dialog.png -------------------------------------------------------------------------------- /images/ue_ribbon_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/ue_ribbon_post.png -------------------------------------------------------------------------------- /images/ue_ribbon_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/ue_ribbon_pre.png -------------------------------------------------------------------------------- /images/vs_external_tools_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/vs_external_tools_dialog.png -------------------------------------------------------------------------------- /images/vs_menu_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/vs_menu_post.png -------------------------------------------------------------------------------- /images/vs_menu_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/vs_menu_pre.png -------------------------------------------------------------------------------- /images/wing_prefs_dialog_command_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/wing_prefs_dialog_command_create.png -------------------------------------------------------------------------------- /images/wing_prefs_dialog_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/wing_prefs_dialog_post.png -------------------------------------------------------------------------------- /images/wing_prefs_dialog_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techartorg/MXSPyCOM/15f1604b87d553c45139f62f595f43241cd870f7/images/wing_prefs_dialog_pre.png -------------------------------------------------------------------------------- /initialize_COM_server.ms: -------------------------------------------------------------------------------- 1 | ( 2 | /* Dynamically writes the necessary Registry information to allow 3 | Simon Felton's MXSCOM bridge to work. 4 | IF RUNNING THIS SCRIPT ON AN VERSION OF MAX OLDER THAN MAX 10 5 | THE AVG EXTENSION *MUST* BE INSTALLED 6 | */ 7 | 8 | local reg_key 9 | local max_version = ((maxVersion())[1] / 1000) as string 10 | 11 | fn create_reg_key hkey key_name ®_key key_value_name key_value_type key_value = 12 | ( 13 | registry.createKey hkey key_name key:®_key 14 | registry.setValue reg_key key_value_name key_value_type key_value 15 | ) 16 | 17 | fn write_sub_key_data reg_key sub_key_name sub_key_type sub_key_value = 18 | ( 19 | local sub_key 20 | registry.createKey reg_key sub_key_name key:&sub_key 21 | registry.setValue sub_key "" sub_key_type sub_key_value 22 | ) 23 | 24 | 25 | -- Establish a root key for generalized Max data 26 | create_reg_key HKEY_CURRENT_USER @"Software\Classes\MAX.Application" ®_key "" #REG_SZ "OLE Automation MAX Application" 27 | 28 | -- Add the Clsid information 29 | write_sub_key_data reg_key "Clsid" #REG_SZ "{7FA22CB1-D26F-11d0-B260-00A0240CEEA3}" 30 | 31 | -- Add the CurVer information 32 | write_sub_key_data reg_key "CurVer" #REG_SZ ("MAX.Application." + max_version) 33 | 34 | -- Establish a new root key for the version of Max being used 35 | create_reg_key HKEY_CURRENT_USER (@"Software\Classes\MAX.Application." + max_version) ®_key "" #REG_SZ ("OLE Automation MAX " + max_version + ".0 Application") 36 | 37 | -- Add the Clsid information 38 | write_sub_key_data reg_key "Clsid" #REG_SZ "{7FA22CB1-D26F-11d0-B260-00A0240CEEA3}" 39 | 40 | -- Make a new root key for the CLSID data 41 | create_reg_key HKEY_CURRENT_USER @"Software\Classes\CLSID\{7FA22CB1-D26F-11d0-B260-00A0240CEEA3}" ®_key "" #REG_SZ ("OLE Automation MAX " + max_version + ".0 Application") 42 | 43 | -- Add sub key data 44 | write_sub_key_data reg_key "ProgID" #REG_SZ ("MAX.Application." + max_version) 45 | write_sub_key_data reg_key "VersionIndependentProgID" #REG_SZ "MAX.Application" 46 | 47 | -- Register the running of files and executing script code to OLE. 48 | registerOLEInterface #( filein, execute, edit, encryptscript ) 49 | ) 50 | -------------------------------------------------------------------------------- /scripts/publish.ps1: -------------------------------------------------------------------------------- 1 | # Build script for MXSPyCOM. 2 | # MXSPyCOM.exe is built in release, with the version number incremented by 0.01, if the user requests. 3 | # MXSPyCOM.exe , the demonstration HelloWorld Python and MaxScript scripts, the initialize_COM_server MaxScript, and 4 | # the readme.md file are archived in an MXSPyCOM.zip file and placed on the desktop, ready for including in a release 5 | # in the GitHub repository. 6 | 7 | [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") 8 | 9 | $inc_ver_num = [System.Windows.Forms.MessageBox]::Show( 10 | "Increment the version number?", "Publish MXSPyCOM", [System.Windows.Forms.MessageBoxButtons]::YesNo) 11 | 12 | if ($inc_ver_num -like "yes") { 13 | [xml]$axml= Get-Content "MXSPyCOM.csproj" 14 | $ver_num_parts = $axml.Project.PropertyGroup.Version.Split(".") 15 | $major = [int]$ver_num_parts[0] 16 | $minor = [int]$ver_num_parts[-1] + 1 17 | if ($minor -gt 99) { 18 | $major += 1 19 | $minor = 00 20 | } 21 | $axml.Project.PropertyGroup.Version = $major.ToString() + "." + $minor.ToString() 22 | $axml.Save("MXSPyCOM.csproj") 23 | } 24 | 25 | dotnet publish "MXSPyCOM.csproj" -c Release 26 | 27 | $temp_path = Join-Path $env:TEMP "MXSPyCOM" 28 | mkdir $temp_path -Force 29 | Copy-Item "bin\release\net6.0-windows\win-x64\publish\MXSPyCOM.exe" $temp_path 30 | Copy-Item "README.md" $temp_path 31 | Copy-Item "hello_world.ms" $temp_path 32 | Copy-Item "hello_world.py" $temp_path 33 | Copy-Item "initialize_COM_server.ms" $temp_path 34 | 35 | $temp_filepath = Join-Path $temp_path "*" 36 | try { 37 | # The user's desktop is in the default location on their hard drive. 38 | $archive_filepath = Join-Path $env:USERPROFILE "DESKTOP" "MXSPyCOM.zip" 39 | Compress-Archive -Path $temp_filepath -DestinationPath $archive_filepath -Force 40 | } 41 | catch { # It would be better if a specific Compress-Archive exception is caught, but none can be found in docs. 42 | # The user's desktop is being managed by OneDrive. 43 | $archive_filepath = Join-Path $env:ONEDRIVE "Desktop" "MXSPyCOM.zip" 44 | Compress-Archive -Path $temp_filepath -DestinationPath $archive_filepath -Force 45 | } 46 | finally { 47 | Remove-Item $temp_path -Recurse -Force 48 | [System.Windows.Forms.MessageBox]::Show( 49 | "Finished. 50 | 51 | Include the MXSPyCOM.zip file that was written to the desktop 52 | in the new binary release on GitHub.", 53 | "Publish MXSPyCOM") 54 | } --------------------------------------------------------------------------------