Files
gtav-src/tools_ng/lib/pipeline/content/treecore.rb
T
2025-09-29 00:52:08 +02:00

613 lines
16 KiB
Ruby
Executable File

#
# File:: treecore.rb
# Description:: Content system core content tree node class implementation.
# Author:: David Muir <david.muir@rockstarnorth.com>
# Author:: Greg Smith <greg@rockstarnorth.com>
# Date:: 12 June 2008
#
#-----------------------------------------------------------------------------
# Uses
#-----------------------------------------------------------------------------
require 'pipeline/log/log'
require 'rexml/document'
include REXML
#-----------------------------------------------------------------------------
# Implementation
#-----------------------------------------------------------------------------
module Pipeline
module Content
#
# == Description
# The Base content node class.
#
# The node's dirty flag can be used by tools to flag certain content nodes
# as being dirty (e.g. Rage convert can use it to ensure that dirty content
# is reconverted).
#
# To read the dirty flag use the dirty? method.
#
class Base
attr_reader :name # String name of content
attr_reader :inputs # Array of Content objects (dependencies)
attr_reader :outputs # Array of Content objects (outputs)
attr_reader :parent # Parent reference
attr_reader :xml_type # XML content type string
XML_CONTENT_TYPE = 'base'
def to_s( )
"CONTENT id:#{self.object_id} name:#{@name} type:#{@xml_type} parent:#{@parent.object_id} inputs(#{@inputs.size}) outputs(#{@outputs.size})"
end
def ===( other )
( ( @name === other.name ) and ( @xml_type == other.xml_type ) )
end
#
# Return dirty flag, virtual attribute.
#
def dirty?()
@dirty
end
#
# Set dirty flag, default to true and not to recurse to input nodes.
#
def set_dirty( dirty = true, recurse = false )
@dirty = dirty
@inputs.each do |input|
input.set_dirty( dirty, recurse )
end
end
#
# Returns content node outdated status.
#
def outdated?( env )
return true if dirty?
@inputs.each do |input|
return true if input.outdated?( env )
end
false
end
def pretty_print( display_inputs = false, indent = 0, indent_char = "\t" )
indent.times do print( indent_char ); end
print( "#{self}\n" )
# Print input nodes...
if ( display_inputs ) then
@inputs.each do |n|
(indent+1).times do print( indent_char ); end
print( "INPUT #{n}\n" )
end
end
end
def fill_env( env )
env.add( 'name', @name ) unless ( nil == @name )
env.add( 'type', @xml_type )
end
#
# Walk a content hierarchy invoking block argument for each content
# node.
#
def walk( &block )
yield self if block_given?
@inputs.each do |input|
input.walk( &block )
end
end
#
# Add a Content node to our array of inputs.
#
def add_input( input )
throw ArgumentError.new( 'Cannot add nil input.' ) \
if ( input.nil? )
throw TypeError.new( "Cannot add non-Content input (class: #{input.class})." ) \
unless ( input.is_a?( Base ) )
@inputs << input
end
# Add a Content node to our array of outputs.
def add_output( output )
throw ArgumentError.new( 'Cannot add nil output.' ) \
if ( output.nil? )
throw TypeError.new( "Cannot add non-Content output (class: #{output.class})." ) \
unless ( output.is_a?( Base ) )
@outputs << output
end
# Virtual method which is invoked after the tree is loaded to resolve
# input nodes.
def post_load_input( )
end
# Virtual method which is invoked after the tree is loaded to resolve
# output nodes.
def post_load_output( )
end
#---------------------------------------------------------------------
# Protected Methods
#---------------------------------------------------------------------
protected
def initialize( name )
@@log = Log.new( 'contenttree' ) if ( nil == @@log )
@name = ( nil == name ) ? '' : name
@inputs = []
@outputs = []
@xml_type = XML_CONTENT_TYPE
@dirty = false
end
# Protected write accessor for 'parent' attribute.
def parent=( p )
@parent = p
end
#---------------------------------------------------------------------
# Dynamic Content Classes Support
# This is the magic that allows the Parser to know about all of our
# defined content types.
#---------------------------------------------------------------------
def self.inherited( child )
# DHM TODO:: do some sanity tests on our content classes before
# registering them into our system. Checks some simple
# requirements, e.g. from_xml class method, type string unique etc.
Base.registered_content_types << child
end
@registered_content_types = []
class << self; attr_reader :registered_content_types end
#---------------------------------------------------------------------
# Protected Variables
#---------------------------------------------------------------------
protected
@@log = nil
end
#
# == Description
# Unresolved objects are created by the parser for inputs that cannot be
# resolved when parsing. As with Unknown objects, these likely indicate a
# misconfiguration in the content XML.
#
class Unresolved < Base
attr_reader :input_name
attr_reader :input_type
attr_reader :input_group
XML_CONTENT_TYPE = 'unresolved'
def initialize( input_name, input_type, input_group )
super( 'unresolved' )
@xml_type = 'UNRESOLVED'
@input_name = input_name
@input_type = input_type
@input_group = input_group
end
def to_s( )
"#{super()} input_name:\"#{input_name}\" input_type:\"#{input_type}\" input_group:\"#{input_group}\""
end
end
#
# == Description
# The SupportChildren module defines attributes and functions for supporting
# an array of child content nodes. This module can then be mixed-in with
# content nodes which require child support that are in a different
# class hierarchy path (e.g. Group, RPF).
#
# This module contains children declaration, walk method and a collection
# of search methods.
#
# NOTE: If you define a class which includes this module you must call the
# init_SupportChildren method or you will get NilClass errors if you access
# members of the children member.
#
module SupportChildren
#don't return the array directly - return a frozen copy
#optimised find_first method relies on the fact that
#the children array is not modified outside of this class
#attr_reader :children # Array of child Content objects
def children
return Array.new(@children).freeze
end
def children=( new_children )
@children = new_children
end
def init_SupportChildren( )
@children = []
end
#
# Set dirty flag, default to true and not to recurse to input nodes.
#
def set_dirty( dirty = true, recurse = false )
super( dirty, recurse )
@children.each do |child|
child.set_dirty( dirty, recurse )
end
end
#
# A node with children is out of date if any of its children are
# outdated
#
def outdated?( env )
@children.each do |child|
return true if child.outdated?( env )
end
false
end
def pretty_print( display_inputs = false, indent = 0, indent_char = "\t" )
indent.times do print( indent_char ); end
print( "#{self}\n" )
if ( display_inputs ) then
@inputs.each do |n|
(indent+1).times do print( indent_char ); end
print( "INPUT #{n}\n" )
end
end
if ( @children.size > 0 )
@children.each do |c|
c.pretty_print( display_inputs, (indent+1), indent_char )
end
else
(indent+1).times do print( indent_char ); end
print( "NO CHILDREN\n" )
end
end
#
# Walk a content hierarchy invoking block argument for each content
# node.
#
def walk( &block )
yield self if block_given?
@children.each do |c|
c.walk( &block )
end
end
#
# Add a child content node into this content group.
#
def add_child( child_content )
throw ArgumentError.new( 'Cannot add nil children.' ) \
if ( child_content.nil? )
throw TypeError.new( "Cannot add non-Content children (class: #{child_content.class})." ) \
unless ( child_content.is_a?( Base ) )
# throw RuntimeError::new( "Cannot add child who already has a parent (parent: #{child_content.parent})." ) \
# unless ( child_content.parent.nil? )
@children << child_content
child_content.parent = self
end
#
# Remove a child content node from this content group.
#
def remove_child( child_content )
@children.delete( child_content )
end
#
# Return an Array of all the found content nodes where the passed block
# evaluates to true.
#
# == Example Usage
# project.content.find( ) do |content|
# content.is_a?( Content::File )
# end
#
def find( &block )
return ( [] ) unless ( block_given? )
found = []
@children.each do |content|
found << content if ( yield content )
# Recurse...
if ( content.methods.include?( 'find' ) )
found += content.find( &block )
end
end
found
end
#
# Similar to 'find' except the objects in the Array are the items
# returned by the block (if non-nil).
#
# == Example Usage
# project.content.collect( ) do |content|
# if ( content.is_a?( Content::File ) )
# content.filename
# else
# nil
# end
# end
#
def collect( &block )
return ( [] ) unless ( block_given? )
found = []
@children.each do |content|
result = ( yield content )
found << result unless ( result.nil? )
# Recurse...
if ( content.methods.include?( 'collect' ) )
found += content.collect( &block )
end
end
found
end
#
# Find first content node with the specified name and type. Returns
# a subclass of Pipeline::Content::Base or nil.
#
def find_first( findname, findtype = '' )
safefindtype = findtype
safefindtype = "_" if findtype == ''
#the following assumes that no children are ever removed...
if @cache == nil then
@cache = Hash.new()
end
if @cache[safefindtype] == nil then
@cache[safefindtype] = Hash.new()
end
if @cache[safefindtype][findname] == nil then
self.walk() { |c|
matchname = ( c.name == findname )
matchtype = ( ( c.xml_type == findtype ) or ( '' == findtype ) )
if ( matchname and matchtype ) then
@cache[safefindtype][findname] = c
return @cache[safefindtype][findname]
end
}
else
return @cache[safefindtype][findname]
end
nil
end
#
# Find first group content node with the specified name. Returns a
# subclass of Pipeline::Content::Base or nil.
#
def find_first_group( name )
find_first( name, 'group' )
end
#
# Return the Group nodes, from this group's content tree downwards,
# that have the specified group name.
#
def find_groups( group )
find_by_script( "('group' == content.xml_type and '#{group}' == content.name)" )
end
#
# Return an Array of all found content nodes that cause the passed in
# script to return true. Nodes are not added to our return set if the
# script return's false for them.
#
# The script can use the keyword 'content' to mean the current content
# node as we walk this content tree.
#
# An empty script will return no content nodes.
#
# E.g.
# # Include all 'file' content type nodes:
# find_by_script( "('file' == content.xml_type)" )
#
# # Include all content types nodes:
# find_by_script( "true" )
#
# # Include no content type nodes:
# find_by_script( "false" )
#
def find_by_script( script )
throw TypeError.new( 'script must be a String' ) \
unless ( script.is_a?( String ) )
return ( [] ) if ( 0 == script.size )
return ( [] ) if ( nil == @children )
found = []
@children.each do |content|
found << content if ( Kernel.eval( script ) )
# Recurse...
if ( content.methods.include?( 'find_by_script' ) )
found += content.find_by_script( script )
end
end
found
end
#
# Similar to 'find_by_script' except the objects in the Array are the
# items returned by the script (if non-nil).
#
def collect_by_script( script )
throw TypeError.new( 'script must be a String' ) \
unless ( script.is_a?( String ) )
return ( [] ) if ( 0 == script.size )
return ( [] ) if ( nil == @children )
found = []
@children.each do |content|
result = ( Kernel.eval( script ) )
found << result unless ( result.nil? )
# Recurse...
if ( content.methods.include?( 'collect_by_script' ) )
found += content.collect_by_script( script )
end
end
found
end
end
#
# == Description
# The Group class is a container for other content nodes (typically
# the nodes are related, but that is arbitary).
#
class Group < Base
include SupportChildren
attr_reader :path # String path of content
attr_writer :path
XML_CONTENT_TYPE = 'group'
def initialize( name, path = '' )
super( name )
init_SupportChildren( )
@xml_type = XML_CONTENT_TYPE
@path = path
end
def to_s( )
"#{super()} path:#{@path} children(#{@children.size})"
end
def fill_env( env )
super( env )
env.add( 'path', @path )
end
def to_xml( )
node = super( )
children.each do |child|
node.add_element( child.to_xml() )
end
node
end
#
# Parse an XML node into a Group object.
#
def Group::from_xml( xml_node, path, env, target )
name = env.subst( xml_node.attributes['name'] ) unless ( xml_node.attributes['name'].nil? )
path = OS::Path.combine( path, xml_node.attributes['path'] )
group = Group.new( name, path )
return ( group )
end
end
#
# == Description
# Level node; equivalent to group but allows finding level directories
# easier.
#
class Level < Group
XML_CONTENT_TYPE = 'level'
def initialize( name, path = '' )
super( name, path )
@xml_type = XML_CONTENT_TYPE
end
def Level.from_xml( xml_node, path, env, target )
name = env.subst( xml_node.attributes['name'] ) unless ( xml_node.attributes['name'].nil? )
path = OS::Path.combine( path, xml_node.attributes['path'] )
level = Level.new( name, path )
return ( level )
end
end
#
# == Description
# Template type, but masqueraded as a group so we can prevent internally
# reprocessing nodes but appear to tools as a regular group.
#
class Template < Group
def initialize( name )
super( name )
end
end
#
# == Description
# Unknown objects are created internally by the parser when an unrecognised
# type string is found.
#
# As with Unresolved objects, these likely indicate a misconfiguration in
# the content XML.
#
class Unknown < Group
attr_reader :reason
attr_reader :xml_node
attr_reader :xml_filename
attr_reader :xml_line
attr_reader :xml_column
XML_CONTENT_TYPE = 'unknown'
def initialize( reason, node )
super( 'UNKNOWN' )
@xml_type = XML_CONTENT_TYPE
@reason = reason
@xml_node = node
@xml_filename = nil
@xml_line = nil
@xml_column = nil
end
def to_s( )
# Only include the first line of XML.
@xml_node =~ /(.*)$/
first_xml_line = $1
"#{super()} reason:\"#{@reason}\" xml_filename:\"#{@xml_filename}\" xml_line:#{@xml_line} xml_column:#{@xml_column} xml:#{first_xml_line}"
end
end
end # Content module
end # Pipeline module
# End of treecore.rb