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

435 lines
17 KiB
Ruby
Executable File

#
# File:: %RS_TOOLSLIB%/util/vfx/data_mk_rmptfx_ipt.rb
# Description:: Build platform-independent RMPTFX IPT file.
#
# Author:: David Muir <david.muir@rockstarnorth.com>
# 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