Files
2025-09-29 00:52:08 +02:00

442 lines
17 KiB
Ruby
Executable File

#
# 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 <adam.munson@rockstarnorth.com>
# 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