# # File:: max_wildwest_regen.rb # Description:: Generates the wildwest menu in max based on the contents of: # ../tools/wildwest/script/max/ # looping through each studio's wildwest folder, using an xml config file # to filter files or override button tooltips etc # # Author:: Adam Munson # Date:: 29 November 2010 # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'pipeline/config/projects' require 'pipeline/os/path' require 'pipeline/os/file' require 'pipeline/os/getopt' require 'pipeline/util/maxscript' require 'pipeline/scm/perforce' require 'rexml/document' include Pipeline include REXML #----------------------------------------------------------------------------- # Constants #----------------------------------------------------------------------------- OPTIONS = [ [ '--wildwest', '-w', OS::Getopt::REQUIRED, 'wildwest directory' ], [ '--help', '-h', OS::Getopt::BOOLEAN, 'display usage information' ] ] # # This takes a menu name from the folder structure and formats it to have # capitals at the start of each word, as well as replacing underscores with # spaces. # # e.g formatName("rockstar_north") => "Rockstar North" # def formatName(n) if(n.include?('_')) then newName = (n.gsub('_',' ').split(' ').each do |part| part.capitalize! end ).join(" ") elsif(n.include?(' ')) then newName = (n.split(' ').each do |part| part.capitalize! end ).join(" ") else newName = n.capitalize end newName end #----------------------------------------------------------------------------- # Implementation #----------------------------------------------------------------------------- if(__FILE__ == $0) then begin g_AppName = OS::Path::get_basename( __FILE__ ) g_Log = Log.new( g_AppName ) g_Config = Pipeline::Config::instance( ) g_Errors = [] #--------------------------------------------------------------------- # Parse Command Line #--------------------------------------------------------------------- g_Opts, g_Trailing = OS::Getopt::getopts( OPTIONS ) if ( g_Opts['help'] ) then puts OS::Getopt::usage( OPTIONS ) exit( 1 ) end wildDir = ( nil == g_Opts['wildwest'] ) ? '' : g_Opts['wildwest'] unless ( ::File::directory?( wildDir ) ) then puts OS::Getopt::usage( OPTIONS ) g_Log.error( "Unable to find Wildwest directory #{wildDir}." ) exit( 2 ) end wildDir = OS::Path::normalise(wildDir) g_Log.info( "Wildwest directory: #{wildDir}" ) #--------------------------------------------------------------------- # Perforce connection and file checkout #--------------------------------------------------------------------- # Set up the Perforce connection con = Pipeline::Config.instance p4 = SCM::Perforce::create( con.sc_tools_server, con.sc_tools_username, con.sc_tools_workspace ) p4.connect unless p4.connected? g_Log.info( "Perforce Server: #{con.sc_tools_server}." ) # Create the changelist change_id = p4.create_changelist( "Auto-regeneration of RS Wildwest" ) p4.run_sync_with_block( "#{OS::Path::get_directory( wildDir )}/..." ) do |filename| g_Log.debug( "\t#{filename}" ) end # Get the installed versions of max so we can run the generation for # multiple versions. Started doing this with Max 2011 and 2012, so ignore # any versions below Max 2011 (version 13.0) autoDesk3dsMax = Autodesk3dsmax::instance() maxVersions = autoDesk3dsMax.versions() maxVersions.delete_if {|v| v.to_f < 13.0 } maxVersions.each do |version| maxRelPath = ('dcc/current/' + Autodesk3dsmax::VERSION_DIRS[version.to_f]) maxPath = OS::Path::combine(con.toolsroot, maxRelPath) mcrPath = OS::Path::combine( maxPath, "UI/MacroScripts/rswildwest.mcr" ) msPath = OS::Path::combine( (maxPath + '/scripts'), "pipeline/rswildwest.ms" ) # Get latest revision g_Log.info( "Getting latest revision..." ) p4.run_sync_with_block( msPath, mcrPath ) do |filename| g_Log.debug( "\t#{filename}" ) end # Check out files and add new files g_Log.info( "Checking out Wildwest files..." ) p4.run_edit_or_add( '-c', change_id.to_s, msPath, mcrPath ) # Open the output macro file and write the header ::FileUtils::rm( mcrPath ) if ( File.exist?( mcrPath ) ) wildMcr = File.new( mcrPath, 'w' ) wildMcr.write( "--\n" ) wildMcr.write( "-- Description:: RS Wildwest Macros\n" ) wildMcr.write( "--\n" ) wildMcr.write( "-- Auto-generated by tools/util/max/max_wildwest_menu.rb\n" ) wildMcr.write( "-- #{Date.today.to_s}\n" ) wildMcr.write( "--\n" ) wildMcr.write("include \"rockstar/export/settings.ms\"\n") wildMcr.write("\n-- Wildwest help link\n") wildMcr.write("macroscript wildwestHelp\n") wildMcr.write("\tcategory:\"RS Wildwest\"\n") wildMcr.write("\tButtonText:\"Wildwest Regeneration Help\"\n") wildMcr.write("(\n\tshellLaunch \"https://devstar.rockstargames.com/wiki/index.php/Wildwest_Tutorial\" \"\"\n)\n") # Open the maxscript file and write the header ::FileUtils::rm( msPath ) if ( File.exist?( msPath ) ) wildMs = File.new( msPath, 'w' ) wildMs.write( "--\n" ) wildMs.write( "-- Description:: RS Wildwest Menu\n" ) wildMs.write( "--\n" ) wildMs.write( "-- Auto-generated by tools/util/max/max_wildwest_regen.rb\n" ) wildMs.write( "-- #{Date.today.to_s}\n" ) wildMs.write( "--\n" ) wildMs.write("\nRsSetMenu \"RS Wildwest\" #(\"wildwestHelp\")\n") # Struct for a file that is specified in config file to be overridden attributeStruct = Struct.new(:file, :toolTip, :buttonText, :icon, :maxscriptBody) # Struct for each menu and the scripts that are to be placed on it menuStruct = Struct.new(:name, :parent, :items, :nameFormatted, :parentFormatted) studioMenus = OS::FindEx::find_dirs( wildDir ) studioMenus.each do |studioWildwestDir| # Get the name of the studio folder studioName = formatName( OS::Path::get_directories(studioWildwestDir).pop ) g_Log.info(studioName + " wildwest folder.") #Should only find one file configFiles = OS::FindEx::find_files( OS::Path::combine( studioWildwestDir, 'wildwestconfig.xml' ) ) if ( configFiles.length > 1 ) then g_Log.error( "Multiple Wildwest config files found in folder for " + studioName ) exit ( 3 ) elsif (configFiles.length == 0 ) then errMsg = ("Wildwest config file not found for " + studioName + " so everything is included") g_Log.error( errMsg ) g_Errors << errMsg else configFile = configFiles[0] g_Log.info("Found config file: " + configFile) end #--------------------------------------------------------------------- # Read XML files/folders to be ignored #--------------------------------------------------------------------- patternsToIgnore = [] if ( configFile ) then configDoc = Document.new File.new(configFile) XPath.each( configDoc, "//config/excludeItems/item" ) { |element| patternsToIgnore << (OS::Path::combine( studioWildwestDir, element.text )) } end # Ignore any old menu.ms files incase they're kicking about patternsToIgnore << "*menu.ms" # Make sure no duplicates just incase patternsToIgnore.uniq g_Log.debug("Patterns to ignore:" + patternsToIgnore.to_s) #--------------------------------------------------------------------- # Filter files and folders #--------------------------------------------------------------------- studioWildwestFiles = OS::FindEx::find_files_recurse( OS::Path::combine(studioWildwestDir, '*.*' ) ) filesToIgnore = [] # Check type of file and match against specified patterns to ignore against all files in # the studio's wildwest folder. studioWildwestFiles.each do |filename| # Delete anything that isn't a maxscript file then move to next file in list unless ( "ms" == OS::Path::get_extension( filename ) ) then filesToIgnore << filename next end patternsToIgnore.each do |pattern| # Check if file matches a given pattern and add to delete list # if it does, then stop checking patterns when first match is found if ( ::File.fnmatch( pattern, filename) == true ) then filesToIgnore << filename break end end end # Remove files if ( filesToIgnore.length > 0 ) then g_Log.debug("Files ignored (pattern matched): " ) filesToIgnore.each do |f| g_Log.debug(f) studioWildwestFiles.delete( f ) end end #--------------------------------------------------------------------- # Check for files to override with attributes from config #--------------------------------------------------------------------- macroscriptOverrides = [] # This list stores the overridden menu items to be removed later, before # we write to the mcr file. filesToRemove = [] # Check for any overrides for attributes in the wildwestconfig file if ( configDoc ) then XPath.each( configDoc, "//config/attributes/macroscript" ) do |element| newStruct = attributeStruct.new() relFilePath = element.attribute('file') if ( relFilePath.nil? or '' == relFilePath.to_s) then errMsg = "No file attribute given in config file for an override - it won't appear on the menu " g_Log.error(errMsg) g_Errors << errMsg else newStruct.file = (OS::Path::combine( studioWildwestDir, relFilePath.to_s)) # Add to wildwest files to make sure the menu structure is created studioWildwestFiles << newStruct.file newStruct.toolTip = element.attribute('toolTip') newStruct.buttonText = element.attribute('buttonText') newStruct.icon = element.attribute('icon') newStruct.maxscriptBody = element.attribute('maxscriptBody') macroscriptOverrides << newStruct filesToRemove << newStruct.file end end end #--------------------------------------------------------------------- # Get menu structure from files and directories #--------------------------------------------------------------------- menus = [] studioWildwestFiles.each do |f| # Splits up the file path to get each directory name below the # studio wildwest folder, then gets the parent and makes sure it's not # already been added to the menu list already. dirs = ( OS::Path::get_directories( f.gsub(wildDir, '') ) ) for i in 0..(dirs.length-1) next unless dirs[i] != '' dirFormatted = formatName(dirs[i]) menu = menuStruct.new(dirs[i], '', [], dirFormatted, '') if dirFormatted == studioName or dirFormatted == '' then menu.parent = "RS Wildwest" menu.parentFormatted = "RS Wildwest" else menu.parent = dirs[i - 1] menu.parentFormatted = formatName(dirs[i-1]) end menus << menu unless menus.include?( menu ) end end # Take a copy of the array so we can delete items from the main # array when looping studioFilesCopy = Array.new(studioWildwestFiles) menus.each do |menu| (studioWildwestFiles.length-1).downto(0) { |i| fileMenuName = OS::Path.get_trailing_directory( studioWildwestFiles[i] ) if( fileMenuName == menu.name ) then if ( menu.parent != "RS Wildwest" ) then next unless studioWildwestFiles[i].include?(menu.parent) end scriptName = OS::Path.get_basename(studioWildwestFiles[i]) # If the script name contains spaces, it won't work in the macroscript so ignore # it and remove it from the copied array so it won't be used later if ( scriptName.include?(' ') )then errMsg = "A script contains space characters which isn't allowed in macroscript, so it was ignored (#{ studioWildwestFiles[i] } )" g_Log.error( errMsg ) g_Errors << errMsg studioFilesCopy.delete_at( i ) else menu.items << scriptName unless menu.items.include?(scriptName) end studioWildwestFiles.delete_at( i ) end } end # Repopulate the files array studioWildwestFiles = Array.new(studioFilesCopy) # Remove the files from the main file list that were overridden filesToRemove.each do |f| (studioWildwestFiles.length-1).downto(0) { |i| if ( studioWildwestFiles[i].include?(f) ) then studioWildwestFiles.delete_at( i ) end } end #--------------------------------------------------------------------- # Write to maxscript file #--------------------------------------------------------------------- # Check if two scripts exist with same name but different parents, # as MAX ignores one even in this situation menus.each do |menu| menus.each do |m| if ( m.name == menu.name and m.parent != menu.parent ) then errMsg = "There are two menus with the same name:#{m.name } in the #{studioName} folder but different parents (#{m.parent} and #{menu.parent}) - MAX will ignore one of these. Change the folder name in perforce and regenerate again to fix this" g_Log.error(errMsg) g_Errors << errMsg end end # Write out the menu and the scripts to place on it wildMs.write("\nRsSetMenu \"#{ menu.nameFormatted }\" #(\n") firstEntry = true menu.items.sort! menu.items.each do |mi| if ( not firstEntry ) then wildMs.write(",") else firstEntry = false end wildMs.write("\n\t\t\"#{ mi }\"") end wildMs.write(") menuParentName:\"#{ menu.parentFormatted }\"\n") end #--------------------------------------------------------------------- # Write to macroscript file #--------------------------------------------------------------------- wildMcr.write("\n-- //////////////////////////////////////////\n") wildMcr.write("-- #{studioName}\n") wildMcr.write("-- //////////////////////////////////////////\n") g_Log.debug("#{studioName} scripts to be used:") studioWildwestFiles.each do |f| g_Log.debug(f) wildMcr.write("\nmacroscript #{OS::Path::get_basename(f)}\n") wildMcr.write("\tcategory:\"#{formatName(OS::Path::get_trailing_directory(f))}\"\n" ) wildMcr.write("\tButtonText:\"#{formatName(OS::Path::get_basename(f))}\"\n" ) wildMcr.write("(\n") wildMcr.write("\tfilein (::RsConfigGetWildwestDir() + \"script/max/#{f.sub(OS::Path::normalise(wildDir), '') }\")\n") wildMcr.write(")\n") end if ( macroscriptOverrides.length > 0 ) then g_Log.info("Files being overridden from config file:") end macroscriptOverrides.each do |o| g_Log.info( o.file ) # Category, button text and the body part of the maxscript is automatically # filled in with defaults if nothing is given in xml file wildMcr.write("\nmacroscript #{ OS::Path::get_basename(o.file) }\n") wildMcr.write("\tcategory:\"#{formatName( OS::Path::get_trailing_directory( o.file ) )}\"\n" ) if ( '' == o.buttonText.to_s ) then wildMcr.write("\tButtonText:\"#{ formatName(OS::Path::get_basename(o.file) ) }\"\n" ) errMsg = "No buttonText attribute given in config file for #{ o.file }, default used" g_Log.error(errMsg) else wildMcr.write("\tButtonText:\"#{ o.buttonText }\"\n" ) end # The following are optional and missed out if nothing present wildMcr.write("\ttooltip:\"#{ o.toolTip }\"\n") unless '' == o.toolTip.to_s wildMcr.write("\tIcon:#{ o.icon }\n") unless '' == o.icon.to_s wildMcr.write("(\n") if ('' == o.maxscriptBody.to_s) then wildMcr.write("\tfilein (::RsConfigGetWildwestDir() + \"script/max/#{o.file.sub( OS::Path::normalise( wildDir ), '') }\")\n") errMsg = "No maxscript body attribute given in config file for #{ o.file }, default used" g_Log.error(errMsg) else wildMcr.write("\t#{ o.maxscriptBody }\n") end wildMcr.write(")\n") end # See which files aren't in perforce so they're marked for add if (studioWildwestFiles.count > 0) then p4.run_add('-c', change_id.to_s, studioWildwestFiles) end end wildMs.close wildMcr.close end puts("\nFinished regenerating wildwest menu\n") if ( g_Errors.length > 0 ) then puts("There were errors:\n\n") g_Errors.each do |e| puts e + "\n\n" end end rescue SystemExit => ex exit( ex.status ) rescue Exception => ex g_Log.exception( ex, 'Unhandled exception' ) puts "Unhandled exception: #{ex.message}" puts ex.backtrace.join("\n") exit -1 end end