387 lines
14 KiB
Ruby
Executable File
387 lines
14 KiB
Ruby
Executable File
#
|
|
# File:: %RS_TOOLSLIB%/pipeline/resourcing/convert.rb
|
|
# Description:: Singleton interface to our convert system.
|
|
#
|
|
# Author:: Greg Smith <greg@rockstarnorth.com>
|
|
# Author:: David Muir <david.muir@rockstarnorth.com>
|
|
# 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
|