# # File:: %RS_TOOLSLIB%/pipeline/resourcing/convert.rb # Description:: Singleton interface to our convert system. # # Author:: Greg Smith # Author:: David Muir # Date:: 13 February 2008 # # This file exposes the public interface to our conversion system. It allows # scripts to start asset pipeline conversions and processing based on the # dynamically loaded set of converters. # # Each converter specifies a content node type that can be converted; using # the inputs to generate that type. # # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'pipeline/os/path' require 'pipeline/config/projects' require 'pipeline/util/rage' require 'pipeline/resourcing/converter' require 'pipeline/resourcing/converters/converter_pack' require 'pipeline/resourcing/converters/converter_zip' require 'pipeline/resourcing/converters/converter_map' require 'pipeline/resourcing/converters/converter_map_scenexml' require 'pipeline/resourcing/converters/converter_animation' require 'pipeline/resourcing/converters/converter_character' require 'pipeline/resourcing/converters/converter_rage' include Pipeline #----------------------------------------------------------------------------- # Implementation #----------------------------------------------------------------------------- module Pipeline module Resourcing # # == Description # ConvertSystem class provides the user-interface to the conversion stage # (Stage 2) of our pipeline; converting game-targeted independent data to # platform specific data. # # The system is initialised with a set of converters that know how to # transform content nodes. # # Before use ensure that you call the +setup+ method. # # == Example Usage # # proj = Pipeline::Config::instance().projects['jimmy'] # convert = Pipeline::ConvertSystem::instance() # convert.setup( proj ) # convert.build( ... ) do |content, success| # ... user-code ... # end # class ConvertSystem include Singleton #--------------------------------------------------------------------- # Attributes #--------------------------------------------------------------------- attr_reader :project attr_reader :branch attr_reader :cache_root, :rebuild, :temporary, :preview attr_reader :converters #--------------------------------------------------------------------- # Methods #--------------------------------------------------------------------- # # Setup the convert system for a specific project and branch. # # If no branch is specified then the project's default branch is used. # def setup( project, branch_name = nil, network = false, rebuild = false, temporary = false, preview = false, load_content = true ) config = Pipeline::Config::instance() project.load_config( ) project.load_content( ) if ( load_content ) @project = project @branch = @project.branches[branch_name] if ( @project.branches.has_key?( branch_name ) ) @branch = @project.branches[@project.default_branch] if ( @branch.nil? ) # Verify we have one or more platforms enabled; if not display an # error telling the user nothing will convert. at_least_one_target = false @branch.targets.each_pair do |platform, target| next unless ( target.enabled ) at_least_one_target = true end if ( ( not at_least_one_target ) and ( not config.user.username.downcase.include?( User::ASSETBUILDER_USER ) ) ) error_msg = "There are no platforms enabled for the current branch.\n" error_msg += "Project: #{@project.uiname}\n" error_msg += "Branch: #{@branch.name}\n\n" error_msg += "Re-run the installer and enabled one or more platforms." GUI::MessageBox::error("Platform Conversion Error Notification", error_msg) return false end @rebuild = rebuild @temporary = temporary @preview = preview @cache_root = "" if @temporary then @cache_root = OS::Path.combine( Config::instance().temp, 'convert', @branch.name ) else @cache_root = OS::Path.combine( project.local, 'convert', @branch.name ) end @converters = [] add_default_converters( ) end # # Builds a passed in content tree, along with any dependencies. The # converters used will yield for each content node that is built specifying # the content node and a bool success status. # # === Example Usage # node = project.content.find_by_group( 'platform' ) # convert.build( node ) do |content, success| # # puts "Error converting: #{content}" unless success # puts "Success converting: #{content}" if success # end # def build( content, &block ) throw ArgumentError::new( "Invalid content node (#{content.class}) or Array." ) \ unless ( content.is_a?( Pipeline::Content::Base ) or content.is_a?( Array ) ) # Handle Array of content nodes also. if ( content.is_a?( Array ) ) then content.each do |c| parse_content( c ); end else parse_content( content ) end # Iterate through the converters; invoking the 'prebuild' step, # then the main build step. Post-process the build step results # as required; ensuring any output is parsed. result = true @converters.each do |converter| converter.prebuild( ) converter_result = converter.build( &block ) throw RuntimeError::new( "Invalid Converter #{converter.class}; returning non-Hash object." ) \ unless ( converter_result.is_a?( Hash ) ) result = false unless ( converter_result[:success] ) # Parse any content that has been passed in from a converter. # This will be picked up by converters that run after the current # one; and allows us to pass things to the RAGE Converter. if ( converter_result.has_key?( :conversion ) ) then throw RuntimeError::new( "Invalid Array of content nodes (#{converter_result[:conversion].class})." ) \ unless ( converter_result[:conversion].is_a?( Array ) ) converter_result[:conversion].each do |c| throw RuntimeError::new( "Invalid ContentNode (#{c.class})." ) \ unless ( c.is_a?( Content::Base ) ) pc = ProjectUtil::data_convert_content_to_platform_content( @project, @branch, c ) pc.each do |p| parse_content( p ); end end end end @converters.each do |converter| converter.clear() end result end # # Return converter output for the last call to 'build'. Iterates # over each converter fetching the required data. # def get_converter_output( cmd = nil, error_regexp = nil, warning_regexp = nil) return '','','' if ( @converters.nil? ) all_errors = '' all_warnings = '' build_output = '' @converters.each do |converter| next unless ( converter.methods.include?( 'get_last_output' ) ) if cmd.nil? build_output << converter.get_last_output( ) else # this will only return errors and warnings full_cmd = "#{cmd} #{converter.log_filename} #{error_regexp} #{warning_regexp}" status, stdout, stderr = systemu(full_cmd) if (stderr) stderr.each do |err| build_output += "#{err}\n" all_errors += "#{err}\n" end end if (stdout) stdout.each do |warn| build_output += "#{warn}\n" all_warnings += "#{warn}\n" end end end end return build_output, all_errors, all_warnings end # # Return the convert system log object. # def ConvertSystem::log( ) @@log = Log.new( 'convert' ) if ( @@log.nil? ) @@log end #--------------------------------------------------------------------- # Protected #--------------------------------------------------------------------- protected @@log = nil CONVERTERS_PATH = '$(toolslib)/pipeline/resourcing/converters/*.rb' # We no longer dynamically load our converters; we want more control # on which ones are loaded. This just iterates through the list of # registered ocnverters and adds them. def add_default_converters( ) # Loop through our registered converters and register them with # the convert pipeline if they appear valid. ConverterBase::registered_converter_types.each do |conv| converter = conv::new( @project, @branch ) if ( ( converter.methods.include?( 'can_convert?' ) ) and ( converter.methods.include?( 'build' ) ) and ( converter.methods.include?( 'clear' ) ) and ( converter.methods.include?( 'add_content' ) ) ) then @converters << converter ConvertSystem::log().info( "Registered Converter #{conv}." ) else ConvertSystem::log().warn( "Ignoring Converter: #{conv} (verification failed)." ) end end end # Now public as accessed in RageConverter. public # Determine whether the content needs converting. This used to be in # each converter but it made chaining dependencies tricky. Its not # centralised so this function may need additions as new content nodes # are added. # # Note: *** these conditions are order dependent *** # def need_convert?( content ) # If the content is a directory we will need to walk it's inputs return ( true ) if content.is_a?( Content::Directory ) return ( false ) if ( 0 == content.inputs.size ) if ( content.is_a?( Content::File ) ) then return ( true ) if ( not File::exists?( content.filename ) ) end # Target dependency check for Ragebuilder timestamp. if ( content.is_a?( Target ) ) then ragebuilder_timestamp = File::mtime( @tools[content.target.platform].path ) return ( true ) if ( ragebuilder_timestamp > File::mtime( content.filename ) ) end # Regenerate this content if we're rebuilding. return ( true ) if ( @rebuild ) # Need to create this content if any of its inputs are going to be # re-created; abstracted from next 'inputs' loop as that checks for # the input file existing first which may not be the case. content.inputs.each do |input| return ( true ) if ( need_convert?( input ) ) end # Need to create this content file if any input is newer than the # existing content file itself. mtime = File::mtime( content.filename ) content.inputs.each do |input| if ( input.is_a?( Content::File ) ) then # If the input file doesn't exist then we need to build # ourselves; we assume it will be there by then. return ( true ) if ( not File::exists?( input.filename ) ) return ( true ) if ( ( File::mtime( input.filename ) <=> mtime ) > 0 ) elsif ( input.is_a?( Content::Group ) ) then # DHM 2011/04/20: this additional Group parsing has # been added for the ped pipeline in 3dsmax. It # constructs Groups as input nodes to build up the # IDD, ILD files etc. Not sure whether the # 'inputs.inputs' recursion is a good idea!! nodes = ( input.children + input.inputs ) nodes.each do |child| next unless ( child.is_a?( Content::File ) ) mtime_child = File::mtime( child.filename ) return ( true ) if ( ( mtime_child <=> mtime ) > 0 ) end end end return ( false ) end protected # Parse content node; adding to the correct converter. def parse_content( content ) throw ArgumentError::new( "Invalid content node (#{content.class})" ) \ unless ( content.is_a?( Pipeline::Content::Base ) ) # There should only be one converter available for each content node # type. We now determine the correct converter to use. converter = nil @converters.each do |c| # Skip converter if it can't convert this content node. next unless ( c.can_convert?( content ) ) converter = c break end if ( converter.nil? ) then # Lets not worry about it; there are plenty content nodes we # do not have a specific converter for. ## ConvertSystem::log().warn( "No converter for #{content.name} (#{content.class})." ) else # We have an allocated converter so lets send it the content # and process the content's inputs too. if ( need_convert?( content ) ) then ConvertSystem::log().info( "Converting: #{content.name} for #{content.target.platform}" ) if ( converter.add_content( content ) ) then ConvertSystem::log().info( "Converter queued: #{content.filename}." ) \ if ( content.is_a?( Content::File ) ) ConvertSystem::log().info( "Converter queued: #{content.name}." ) \ unless ( content.is_a?( Content::File ) ) # Parse content outputs; we're typically passed an input # node so need to walk down the tree. content.outputs.each do |output| parse_content( output ) end else ConvertSystem::log().warn( "Converter #{converter} discarded content: #{content}." ) end else #ConvertSystem::log().info( "No need to convert content: #{content.filename}." ) \ # if ( content.is_a?( Content::File ) ) #ConvertSystem::log().info( "No need to convert content: #{content.name}." ) \ # unless ( content.is_a?( Content::File ) ) end end if content.class <= Pipeline::Content::SupportChildren then content.children.each do |c| ConvertSystem::log().error( "Content node nil parent: #{content}" ) \ if ( c.nil? ) parse_content( c ) end end end end end # Resourcing module end # Pipeline module # %RS_TOOLSLIB%/pipeline/resourcing/convert.rb