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

505 lines
14 KiB
Ruby
Executable File

#
# File:: maxscript.rb
# Description:: MaxScript Interface
#
# Author:: Marissa Warner-Wu <marissa.warner-wu@rockstarnorth.com>
# Date:: 6 August 2009
#
require 'pipeline/os/file'
require 'pipeline/resourcing/path'
require 'pipeline/config/projects'
require "rexml/document"
include REXML
#-----------------------------------------------------------------------------
# Implementation
#-----------------------------------------------------------------------------
module Pipeline
#
# == Description
# Simple interface for working with and parsing MaxScript files.
#
class Maxscript
#---------------------------------------------------------------------
# Public Constants
#---------------------------------------------------------------------
@@toolsroot = Pipeline::Config::instance().toolsroot
@@dir_max = 'dcc/current/max2011'
@@dir_scripts = 'dcc/current/max2011/scripts'
#---------------------------------------------------------------------
# Class Methods
#---------------------------------------------------------------------
#
# == Description
# Returns the current max directory.
#
def Maxscript::dir_max
OS::Path::combine( @@toolsroot, @@dir_max )
end
#
# == Description
# Returns the current script directory.
#
def Maxscript::dir_scripts
OS::Path::combine( @@toolsroot, @@dir_scripts )
end
#
# == Description
# Reformats a file path the same way it would appear in a MaxScript file.
# The reverse of full_path().
#
# == Example Usage
# Maxscript.format_path( "x:/pipedev/tools/dcc/current/max2009/scripts/rockstar/toolstest.ms" )
# => "/rockstar/toolstest.ms"
#
def Maxscript::format_path( filepath )
OS::Path::normalise( filepath.split( @@dir_scripts )[1] )
end #format_path
#
# == Description
# Fills out the Max filepath format used in file includes to create a full
# path. If no toolsroot is given it uses the system default. The reverse
# of format_path().
#
# == Example Usage
# Maxscript.full_path( "/rockstar/toolstest.ms" )
# => "x:/pipedev/tools/dcc/current/max2009/scripts/rockstar/toolstest.ms"
#
def Maxscript::full_path( filepath, toolsroot = nil)
# Get the toolsroot
if toolsroot.nil?
scriptDir = Maxscript.dir_scripts
else
scriptDir = OS::Path::combine( toolsroot, @@dir_scripts )
end
OS::Path::combine( scriptDir, filepath )
end #full_path
#
# == Description
# Given a tools root, returns a list of all the MaxScript files
# that it can find in the @@dir_scripts folder. If no tools root is
# given it uses the system default.
#
# == Example Usage
# Maxscript.find_files( "x:/" )
# => ["filepath1", "filepath2", etc.]
#
def Maxscript::find_files( toolsroot=@@toolsroot )
# Get the script directory
scriptDir = Maxscript.dir_scripts
throw RuntimeError.new( "Unable to find MaxScript directory #{scriptDir}." ) unless ::File::directory?( scriptDir )
# Get all the maxscript files
ms_files = OS::FindEx::find_files_recurse( OS::Path::combine( scriptDir, '*.ms' ) )
throw RuntimeError.new( "Unable to find any MaxScript files in #{scriptDir}." ) if ms_files.empty?
ms_files
end #find_files
#
# == Description
# Determines whether this is a valid MaxScript file. Only
# accepts .ms or .mcr files.
#
# == Example Usage
# Suppose "x:/toolstest.ms" is a MaxScript file.
#
# Maxscript.valid_file?( "x:/toolstest.ms" )
# => true
# Maxscript.valid_file?( "x:/toolstest.txt" )
# => false
#
def Maxscript::valid_file?( filename )
valid = false
if (File.exist? filename)
case OS::Path::get_extension( filename )
when 'ms' then valid = true
when 'mcr' then valid = true
else puts "ERROR: #{OS::Path::get_extension( filename )} not a valid MaxScript extension."
end #case OS::Path::get_extension( filename )
else
puts "ERROR: The file #{filename} is not valid because it does not exist."
end #if (File.exist? filename)
valid
end #valid_file?
#
# == Description
# Determines whether a given line is a comment.
#
# == Example Usage
# Maxscript.line_is_comment?( "-- this is a comment" )
# => true
#
def Maxscript::line_is_comment?( line )
comment = false
l = line.strip
# Are the first two characters of this line "-"?
if (l[0] and l[1]) == 45
comment = true
end
comment
end #line_is_comment?
#
# == Description
# Determines whether a given line is a function declaration.
#
# == Example Usage
# Maxscript.line_is_function?( "fn RsFunction1 var =(" )
# => true
#
def Maxscript::line_is_function?( line )
function = false
l = line.strip
if (line.include? "fn") or (line.include? "function")
function = true
end
function
end #line_is_function?
#
# == Description
# Determines whether a given line is a file include.
#
# == Example Usage
# Maxscript.line_is_filein?( "filein 'rockstar/toolstest.ms'" )
# => true
#
def Maxscript::line_is_filein?( line )
filein = false
l = line.strip
if (line.include? "filein") or (line.include? "include")
filein = true
end
filein
end #line_is_filein?
#
# == Description
# Determines whether a given line defines a rollout.
#
# == Example Usage
# Maxscript.line_is_rollout?( "rollout RsToolsTest 'Tools Test'" )
# => true
#
def Maxscript::line_is_rollout?( line )
rollout = false
l = line.strip
if (line.include? "rollout")
rollout = true
end
rollout
end #line_is_rollout?
#
# == Description
# Returns the contents of MaxScript file. Also tests to make
# sure that the file is valid and escapes all doublequotes.
#
# == Example Usage
# Maxscript.get_lines( "x:/toolstest.ms" )
# => ["line1", "line2", etc.]
#
def Maxscript::get_lines( filename )
throw ArgumentError.new("The file #{filename} is not a valid MaxScript file.") unless Maxscript.valid_file?( filename )
# Get the text
text = []
File.open(filename, 'r') do
text = IO.readlines( filename )
end
# Escape any double quotes
text.each { |line| line.tr!('"', '\"') }
text
end #get_lines
#
# == Description
# Determines whether file1 is really used in file2 by comparing the
# list of functions in the first with the function calls in the second.
# Returns true or false.
#
# == Example Usage
# file1 = "x:/toolstest.ms"
# file2 = "x:/myscript.ms"
# Maxscript.file_is_used?( file1, file2 )
# => true
#
def Maxscript::file_is_used?( file1, file2 )
# Set up our variables
used = false
# Compare function usage
functions = Maxscript.get_functions( file1 )
usage = Maxscript.string_usage_hash( functions, file2 )
used = true if usage.has_value? true
# If we couldn't find a use then check the rollouts as well
unless used
rollouts = Maxscript.get_rollouts( file1 )
usage = Maxscript.string_usage_hash( rollouts, file2 )
used = true if usage.has_value? true
end
used
end #file_is_used?
#
# == Description
# Returns the name of the file included in a line using "filein"
# and "include". Reformats path names by normalising them and making
# sure they always begin with a "/" character. If no valid file
# include is found it returns a blank string. Takes the line itself and
# the name of the file where it appears.
#
# == Example Usage
# Maxscript.get_included_file( "filein 'toolstest.ms'", "x:/pipedev/tools/dcc/current/max2009/scripts/rockstar/myscript.ms" )
# => "/rockstar/toolstest.ms"
#
def Maxscript::get_included_file( line, ref_file="" )
filename = ""
# Is this a line we care about?
unless Maxscript.line_is_comment?( line )
if Maxscript.line_is_filein?( line )
filename = OS::Path::normalise( line.split('"')[1] )
filename.strip!
# Check to see whether this path is relative
relpath = OS::Path::combine( OS::Path::remove_filename( ref_file ), filename )
if File.exist? relpath
if ref_file.empty?
puts "\n\nERROR: No referencing file provided for a relative path!\n\n"
else
filename = Maxscript.format_path( relpath )
end
else
# If the path isn't relative then make sure there's a "/" in front
filename.insert(0, "/") unless ( filename[0] == "/" )
end #if OS::Path::get_directories( filename ).empty?
end #Maxscript.line_is_filein?
end
filename
end #get_included_file
#
# == Description
# Returns a list of all the files included in a file.
#
# == Example Usage
# Maxscript.get_included_files( "x:/toolstest.ms" )
# => ["filepath1", "filepath2"]
#
def Maxscript::get_included_files( filename )
# Set up our variables
text = Maxscript.get_lines( filename )
includes = []
# Parse each line
text.each do |line|
unless Maxscript.line_is_comment?( line )
filein = Maxscript.get_included_file( line, filename )
includes << filein unless filein.empty?
end
end #text.each
includes
end #get_included_files
#
# == Description
# Returns a list of all the functions defined in a file.
#
# == Example Usage
# Maxscript.get_functions( "x:/toolstest.ms" )
# => ["RsFunction1", "RsFunction2"]
#
def Maxscript::get_functions( filename )
# Set up our variables
text = Maxscript.get_lines( filename )
functions = []
# Parse each line
text.each do |line|
# Is this a line we care about?
unless Maxscript.line_is_comment?( line )
if Maxscript.line_is_function?( line )
fnName = line.split[1]
next if fnName.nil?
fnName.strip!
functions << fnName
end
end
end #text.each
functions
end #get_functions
#
# == Description
# Returns a list of all the rollouts defined in a file.
#
# == Example Usage
# Maxscript.get_rollouts( "x:/toolstest.ms" )
# => ["RsRollout1", "RsRollout2"]
#
def Maxscript::get_rollouts( filename )
# Set up our variables
text = Maxscript.get_lines( filename )
rollouts = []
# Parse each line
text.each do |line|
# Is this a line we care about?
unless Maxscript.line_is_comment?( line )
if Maxscript.line_is_rollout?( line )
rollName = line.split[1]
next if rollName.nil?
rollName.strip!
rollouts << rollName
end
end
end #text.each
rollouts
end #get_rollouts
#
# == Description
# Takes a list of strings and tells whether these
# are in a file. Returns a hash with the strings
# names as keys and whether or not it was found. For
# more detailed information, use string_usage_xml().
#
# == Example Usage
# funcs = ["RsFunction1", "RsFunction2"]
# Maxscript.string_usage_hash( funcs, "x:/myscript.ms" )
# => {"RsFunction1" => true, "RsFunction2" => false}
#
def Maxscript::string_usage_hash( find_strings, filename )
# Set up our variables
text = Maxscript.get_lines( filename )
usage = {}
find_strings.each { |phrase| usage[phrase] = false }
# Parse each line
text.each do |line|
# Is this a line we care about?
unless Maxscript.line_is_comment?( line )
# Check each string against this line
find_strings.each do |phrase|
if line.include? phrase
# If we found a match, set the usage to true
usage[phrase] = true
end
end #find_strings.each
end #unless Maxscript.line_is_comment?
end #text.each
usage
end #string_usage_hash
#
# == Description
# Takes a list of strings and tells whether these
# are in a file. Returns an REXML element which is the
# root node of the hierarchy for easy parsing. Essentially
# a more verbose version of string_usage_hash().
#
# == Example Usage
# Suppose RsFunction1 is used in line 230 of myscript.ms and
# RsFunction2 is not used.
#
# funcs = ["RsFunction1", "RsFunction2"]
# Maxscript.string_usage_xml( funcs, "x:/myscript.ms" )
# =>
# <file name ="x:/myscript.ms">
# <string value = "RsFunction1">
# <line>230</line>
# </string>
# </file>
#
def Maxscript::string_usage_xml( find_strings, filename )
# Create the file element
file_elem = Element.new( 'file' )
file_elem.add_attribute( 'name', filename )
# Read the file and start parsing it
text = Maxscript.get_lines( filename )
text.each_index do |index|
# Is this a line we care about?
unless Maxscript.line_is_comment?( text[index] )
# Check each phrase against this line
find_strings.each do |phrase|
if text[index].include? phrase
line_number = index + 1
# If we found a match, check to see whether we already have a node for this phrase
line_elem = nil
file_elem.each_element_with_attribute( 'value', phrase ) do |element|
line_elem = element.add_element( 'line' )
line_elem.add_text( line_number.to_s )
end
# If we were unable to find a matching element, create one
unless line_elem
text_elem = file_elem.add_element( 'string' )
text_elem.add_attribute( 'value', phrase )
line_elem = text_elem.add_element( 'line' )
line_elem.add_text( line_number.to_s )
end
end #if text[index].include? fn
end #find_strings.each
end #unless Maxscript.line_is_comment?
end #text.each
file_elem
end #string_usage_xml
end #Maxscript class
end #Pipeline module