# # File:: %RS_TOOLSLIB%/util/vfx/data_mk_rmptfx_ipt.rb # Description:: Build platform-independent RMPTFX IPT file. # # Author:: David Muir # Date:: 18 October 2012 (AP3, IronRuby) # Date:: 3 June 2008 (original) # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'RSG.Base.dll' require 'RSG.Base.Configuration.dll' require 'RSG.Base.Windows.dll' require 'RSG.Pipeline.Core.dll' require 'RSG.Pipeline.Content.dll' require 'RSG.Pipeline.Services.dll' require 'RSG.SourceControl.Perforce.dll' include RSG::Base::Collections include RSG::Base::Configuration include RSG::Base::Logging include RSG::Base::Logging::Universal include RSG::Base::OS include RSG::Base::Windows include RSG::Pipeline::Services::Platform::Texture include RSG::SourceControl::Perforce require 'mscorlib' require 'System.Core' include System::Collections::Generic include System::IO using_clr_extensions System::Linq require 'pipeline/monkey/object' require 'pipeline/monkey/string' require 'pipeline/os/options' include Pipeline #----------------------------------------------------------------------------- # Constants #----------------------------------------------------------------------------- AUTHOR = 'RSGEDI Tools' EMAIL = 'RSGEDI Tools <*tools@rockstarnorth.com>' OPTIONS = [ LongOption::new( 'fxlist', LongOption::ArgType.Required, 'f', 'path to IPT FX list.' ), LongOption::new( 'ipt', LongOption::ArgType.Required, 'i', 'IPT basename (e.g. core, core_e1).' ), LongOption::new( 'output', LongOption::ArgType.Required, 'o', 'path to output IPT file.' ), LongOption::new( 'textures', LongOption::ArgType.None, 't', 'build texture dictionary only.' ), LongOption::new( 'metadata', LongOption::ArgType.Required, 'm', 'texture metadata location (tcs files).' ), LongOption::new( 'template', LongOption::ArgType.Required, 'p', 'texture template for new tcs files' ), LongOption::new( 'regentcs', LongOption::ArgType.None, 'z', 'force regeneration of texture templates' ), LongOption::new( 'reconcile', LongOption::ArgType.None, 'r', 'enable Perforce reconcile offline work (default: false)' ), LongOption::new( 'override', LongOption::ArgType.Required, 'v', 'override directory' ), LongOption::new( 'tag', LongOption::ArgType.Optional, '1', 'override tag directory, for hi/lo vfx' ) ] CL_DESCRIPTION = 'Automatic VFX IPT asset build script:' #----------------------------------------------------------------------------- # Functions #----------------------------------------------------------------------------- def read_list_from_file( filename ) list = [] File::open( filename, 'r' ) do |fp| fp.each do |line| list << line.chomp.strip unless ( line.starts_with( '#' ) ) end end list end def read_list_from_dir( path ) list = [] Dir::open( path ) do |dir| dir.each do |filename| next if ( '.' == filename ) next if ( '..' == filename ) list << filename end end list end # Create texture TCL data as required. def create_tcl( log, texture_filename, template, metadata_path, prepend, iptname, temp_dir ) texture_basename = Path::GetFileNameWithoutExtension( texture_filename ) ipt_basename = Path::GetFileNameWithoutExtension( iptname ) tcl_tempdir = Path::Combine( temp_dir, ipt_basename ) tcsname = Path::ChangeExtension( texture_basename, ".tcs" ) tcspath = Path::Combine( metadata_path, tcsname ) tcsdir = Path::GetDirectoryName( tcspath ) tclname = Path::ChangeExtension( texture_basename, ".tcl" ) tclpath = Path::Combine( tcl_tempdir, tclname ) tcldir = Path::GetDirectoryName( tclpath ) Directory::CreateDirectory( tcldir ) unless ( Directory::Exists( tcldir ) ) source_textures = [].to_clr( System::String ) if ( not System::IO::File::Exists( tclpath ) ) then log.Message( "Creating TCL: #{tclpath}" ) SpecificationFactory::CreateLink( tclpath, tcspath ) end dst = "#{prepend}#{tclname}" Pair[System::String, System::String]::new( tclpath, dst ) end # Clean up empty changelists with a particular description. def cleanup_changelists( log, p4, config ) changes = p4.Run( 'changes', false, ['-L', '-u', config.Username, '-s', 'pending', '-c', p4.Client].to_clr(System::String) ) changes.each do |change| next unless ( change['desc'].include?( CL_DESCRIPTION ) ) ## log.Warning( "Removing empty changelist #{change['change']}." ) p4.Run( "change", false, ['-d', change['change'].to_s].to_clr(System::String) ) end end # Print usage information to stdout. def usage( options ) puts "#{__FILE__}" puts "Usage:" puts options.usage() exit( 1 ) end #----------------------------------------------------------------------------- # Entry-Point #----------------------------------------------------------------------------- if ( __FILE__ == $0 ) then # Initialise log and console output. LogFactory.Initialize() g_Log = LogFactory.CreateUniversalLog('data_mk_rmptfx_ipt') LogFactory.CreateApplicationConsoleLogTarget( ) g_Options = OS::Options::new( OPTIONS ) begin if ( g_Options.show_help? ) usage( g_Options ) end g_Config = RSG::Base::Configuration::ConfigFactory::CreateConfig( ) g_BranchName = g_Options.has_option?( 'branch' ) ? g_Options.get( 'branch' ) : g_Config.Project.DefaultBranchName g_Branch = g_Config.Project.Branches[g_BranchName] if ( g_Config.Project.Branches.ContainsKey( g_BranchName ) ) if ( g_Branch.nil? ) then g_Log.Error( "Invalid branch '#{g_BranchName}' for project '#{g_Config.Project.UIName}'." ) exit( 1 ) end #--------------------------------------------------------------------- # Parse Command Line #--------------------------------------------------------------------- g_Output = g_Options.has_option?('output') ? ::File::expand_path( g_Options.get('output') ) : nil if ( g_Output.nil? ) then puts 'No output ipt file specified.' usage( g_Options ) exit( 5 ) end g_FxList = g_Options.has_option?('fxlist') ? ::File::expand_path( g_Options.get('fxlist') ) : nil if ( g_FxList.nil? or ( not ::File::directory?( g_FxList ) ) ) then puts 'No or invalid input FX List directory specified.' usage( g_Options ) exit( 6 ) end g_Ipt = g_Options.has_option?('ipt') ? g_Options.get('ipt') : nil if ( g_Ipt.nil? ) then puts 'No ipt basename specified.' usage( g_Options ) exit( 7 ) end # trying to retrieve the tag for hi/lo redirection and potential override g_Tag = g_Options.has_option?('tag') ? g_Options.get('tag') : nil if ( g_Tag.nil? ) then puts 'No tag specified, falling back to default' else puts "Tag specified : #{g_Tag}" end g_OverrideDir = g_Options.get('override') g_TexturesOnly = g_Options.get('textures') g_regentcs = g_Options.get('regentcs') g_metadata_path = g_Options.get('metadata') g_metadata_template = g_Options.get('template') g_P4Reconcile = g_Options.has_option?( 'reconcile' ) ? g_Options.get('reconcile') : false #--------------------------------------------------------------------- # Process our lists for dependencies #--------------------------------------------------------------------- root = Path::GetDirectoryName( g_FxList ) list_effects_filename = Path::Combine( g_FxList, g_Ipt, "#{g_Ipt}.effectlist" ) # ME list_emit_filename = Path::Combine( g_FxList, g_Ipt, "#{g_Ipt}.emitlist" ) # ME list_model_filename = Path::Combine( g_FxList, g_Ipt, "#{g_Ipt}.modellist" ) list_ptx_filename = Path::Combine( g_FxList, g_Ipt, "#{g_Ipt}.ptxlist" ) # ME list_texture_filename = Path::Combine( g_FxList, g_Ipt, "#{g_Ipt}.texlist" ) list_fxlists = read_list_from_dir( Path::Combine( g_FxList, g_Ipt ) ) list_effectrules = read_list_from_file( list_effects_filename ) list_emitrules = read_list_from_file( list_emit_filename ) list_models = read_list_from_file( list_model_filename ) list_ptxrules = read_list_from_file( list_ptx_filename ) list_textures = read_list_from_file( list_texture_filename ) Directory::CreateDirectory( Path::GetDirectoryName( g_Output ) ) if ( !Directory::Exists( Path::GetDirectoryName( g_Output ) ) ) iptname = Path::GetFileNameWithoutExtension( g_Output ) temp_dir = Path::Combine( g_Config.ToolsTemp, "metadata" ) Directory::Delete( temp_dir, true ) if ( Directory::Exists( temp_dir ) ) Directory::SetCurrentDirectory( g_Config.Project.Root ) g_P4 = g_Config.Project.SCM if ( g_P4Reconcile ) g_Changelist = g_P4.CreatePendingChangelist( "#{CL_DESCRIPTION} #{iptname}" ) if ( g_P4Reconcile ) g_ChangelistNumber = g_Changelist.Number if ( g_P4Reconcile ) g_DepLists = System::Collections::Generic::List[Pair[System::String, System::String]]::new( ) g_FileList = System::Collections::Generic::List[Pair[System::String, System::String]]::new( ) if ( g_TexturesOnly ) then #----------------------------------------------------------------- # Build Texture Dictionary Only #----------------------------------------------------------------- list_models.each do |model| modelname = model.split( '/' )[0] modeldir = OS::Path::combine( root, 'models', modelname ) Dir::open( modeldir ) do |dir| dir.each do |filename| next if ( '.' == filename ) next if ( '..' == filename ) fullpath = OS::Path::combine( modeldir, filename ) if ( '.dds' == Path::GetExtension( fullpath ) ) then # Add modeltex file entry = {} entry[:src] = fullpath entry[:dst] = filename g_FileList << entry end end end end list_textures.each do |texture| fullname = "#{texture}.dds" fullpath = Path::Combine( root, 'textures', fullname ) entry = Pair::new( fullpath, fullname ) g_FileList << entry g_FileList << create_tcl( g_Log, fullpath, g_metadata_template, g_metadata_path, "", iptname, temp_dir ) end else #----------------------------------------------------------------- # Build Full IPT RPF #----------------------------------------------------------------- g_Log.Message( "Adding #{list_fxlists.size} fxlists..." ) list_fxlists.each do |fxlist| src = Path::Combine( root, 'fxlists', g_Ipt, fxlist ).to_clr_string dst = "fxlists/#{g_Ipt}/#{fxlist}".to_clr_string entry = Pair[System::String, System::String]::new( src, dst ) g_DepLists << entry end g_Log.Message( "Adding #{list_effectrules.size} effect rules..." ) list_effectrules.each do |effectrule| fullname = "#{effectrule}.effectrule" if g_Tag.nil? src = Path::Combine( root, 'effectrules', fullname ).to_clr_string else src = Path::Combine( root, 'effectrules', g_Tag, fullname ).to_clr_string unless File.exist?( src ) src = Path::Combine( root, 'effectrules', fullname ).to_clr_string else print "effectrules #{src} \n" end end dst = "effectrules/#{fullname}" srcoverride = Path::Combine( root, 'effectrules', g_OverrideDir, fullname ) if g_OverrideDir != nil if srcoverride != nil and File.exist?(srcoverride) then g_Log.Message( "Effect rule override has been found for #{g_OverrideDir} at #{srcoverride}" ) g_FileList << Pair[System::String, System::String]::new( srcoverride, dst ) else g_FileList << Pair[System::String, System::String]::new( src, dst ) end end g_Log.Message( "Adding #{list_emitrules.size} emit rules..." ) list_emitrules.each do |emitrule| fullname = "#{emitrule}.emitrule" if g_Tag.nil? src = Path::Combine( root, 'emitrules', fullname ).to_clr_string else src = Path::Combine( root, 'emitrules', g_Tag, fullname ).to_clr_string unless File.exist?( src ) src = Path::Combine( root, 'emitrules', fullname ).to_clr_string end end dst = "emitrules/#{fullname}" srcoverride = Path::Combine( root, 'emitrules', g_OverrideDir, fullname ) if g_OverrideDir != nil if srcoverride != nil and File.exist?(srcoverride) then g_Log.Message( "Emit rule override has been found for #{g_OverrideDir} at #{srcoverride}" ) g_FileList << Pair[System::String, System::String]::new( srcoverride, dst ) else g_FileList << Pair[System::String, System::String]::new( src, dst ) end end g_Log.Message( "Adding #{list_models.size} models..." ) list_models.each do |model| modelname = model.split('/')[0] modeldir = Path::Combine( root, 'models', modelname ) Dir::open( modeldir ) do |dir| dir.each do |filename| next if ( '.' == filename ) next if ( '..' == filename ) fullpath = Path::Combine( modeldir, filename ) if ( '.dds' == Path::GetExtension( fullpath ) ) then # Add modeltex file src = fullpath dst = "modeltex/#{filename}" entry = Pair[System::String, System::String]::new( src, dst ) g_FileList << entry else # Add model file src = fullpath dst = "models/#{modelname}/#{filename}" entry = Pair[System::String, System::String]::new( src, dst ) g_FileList << entry end end end end g_Log.Message( "Adding #{list_ptxrules.size} PTX rules..." ) list_ptxrules.each do |ptxrule| fullname = "#{ptxrule}.ptxrule" if g_Tag.nil? src = Path::Combine( root, 'ptxrules', fullname ).to_clr_string else src = Path::Combine( root, 'ptxrules', g_Tag, fullname ) unless File.exist?( src ) src = Path::Combine( root, 'ptxrules', fullname ) end end dst = "ptxrules/#{fullname}" srcoverride = Path::Combine( root, 'ptxrules', g_OverrideDir, fullname ) if g_OverrideDir != nil if srcoverride != nil and File.exist?(srcoverride) then g_Log.Message( "PTX rule override has been found for #{g_OverrideDir} at #{srcoverride}" ) g_FileList << Pair[System::String, System::String]::new( srcoverride, dst ) else g_FileList << Pair[System::String, System::String]::new( src, dst ) end end g_Log.Message( "Adding #{list_textures.size} textures..." ) list_textures.each do |texture| fullname = "#{texture}.dds" fullpath = Path::Combine( root, 'textures', fullname ) if ( File::exists?( fullpath ) ) then src = fullpath dst = "textures/#{fullname}" entry = Pair[System::String, System::String]::new( src, dst ) g_FileList << entry g_FileList << create_tcl( g_Log, fullpath, g_metadata_template, g_metadata_path, "textures/", iptname, temp_dir ) end end end # Perforce check that everything is in the depot. if ( g_P4Reconcile and g_FileList.size > 0 ) then g_Log.Message( "Reconciling offline work with Perforce depot..." ) files = g_FileList.to_seq files = files.Where( lambda { |p| ".tcl" != Path::GetExtension(p.First) } ). Select( lambda { |p| Path::GetFullPath( p.First.ToLower() ) } ).to_a.to_clr( System::String ) g_Log.Message( "#{files.Count()} source files..." ) fileStates = FileState::Create( g_P4, files ) filesInP4 = fileStates.Select( lambda { |f| Path::GetFullPath( f.ClientFilename.ToLower() ) } ) filesNotInP4 = files.Except( filesInP4 ) g_Log.Message( "#{files.Count()} files, #{filesInP4.Count()} already in Perforce, #{filesNotInP4.Count()} needing added." ) filesNotInP4.each do |f| g_P4.Run( "add", false, ['-c', g_ChangelistNumber.to_s, f].to_clr(System::String) ) end end # Perforce revert unchanged; delete changelist if empty. g_P4.Run( "revert", false, ['-a', '-c', g_ChangelistNumber.to_s].to_clr(System::String) ) if ( g_P4Reconcile ) cleanup_changelists( g_Log, g_P4, g_Config ) if ( g_P4Reconcile ) if ( System::IO::File::Exists( g_Output ) ) then g_Log.Message( "Deleting existing output: #{g_Output}" ) System::IO::File::Delete( g_Output ) end filelist = g_FileList.AddRange( g_DepLists ).to_a g_Log.Message( "Creating: #{g_Output}..." ) Zip::Create( g_Branch, g_Output, (g_FileList + []).to_clr( Pair[System::String,System::String] ) ) g_Log.Message( "Done." ) rescue SystemExit => ex exit( ex.status ) rescue Exception => ex g_Log.Exception( ex, "Exception during #{__FILE__}." ) puts "Exception during #{__FILE__}: #{ex.message}" puts ex.backtrace.join("\n") unless ( g_Options.is_enabled?( 'nopopups' ) ) then dlg = RSG::Base::Windows::ExceptionStackTraceDlg::new( ex, g_Config.EmailAddress, AUTHOR, EMAIL ) dlg.ShowDialog( ) end exit( 1 ) end end # data_mk_rmptfx_ipt.rb