Advertisement
Guest User

Source Filmmaker - "VALVE FILESYSTEM INTEGRATION"

a guest
Aug 5th, 2012
888
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 29.78 KB | None | 0 0
  1. ########### VALVE FILESYSTEM INTEGRATION ###########
  2.  
  3. from path import Path, PathError
  4.  
  5. import os
  6. import sys
  7.  
  8. ### Platform
  9. WIN_32_SUFFIX = 'win32'
  10. WIN_64_SUFFIX = 'win64'
  11.  
  12. #make sure there is a HOME var...
  13. try:
  14.     os.environ['HOME'] = os.environ['USERPROFILE']
  15. except KeyError:
  16.     os.environ['HOME'] = str( Path( '%HOMEDRIVE%/%HOMEPATH%' ) )
  17.  
  18. _MOD = None
  19. def mod():
  20.     '''
  21.     returns the mod name of the current project
  22.     '''
  23.     global _MOD
  24.     try:
  25.         _MOD = Path( os.environ[ 'VPROJECT' ] ).name()
  26.         return _MOD
  27.     except KeyError:
  28.         raise KeyError( '%VPROJECT% not defined' )
  29.  
  30. _GAME = None
  31. def game():
  32.     '''
  33.     returns a Path instance representing the %VGAME% path - path construction this way is super easy:
  34.     somePropPath = game() / mod() / 'models/props/some_prop.dmx'
  35.     '''
  36.     global _GAME
  37.     try:
  38.         _GAME = Path.Join( os.environ[ 'VPROJECT' ], '..' )
  39.         return _GAME
  40.     except KeyError:
  41.         raise KeyError( '%VPROJECT% not defined.' )
  42.     except PathError:
  43.         raise PathError( '%VPROJECT% is defined with an invalid path.' )
  44.  
  45. _CONTENT = None
  46. def content():
  47.     '''
  48.     returns a Path instance representing the %VCONTENT% path - path construction this way is super easy:
  49.     somePropPath = content() / 'ep3/models/characters/alyx/maya/alyx_model.ma'
  50.     '''
  51.     global _CONTENT
  52.  
  53.     try:
  54.         return Path( os.environ[ 'VCONTENT' ] )
  55.     except KeyError:
  56.         try:
  57.             _CONTENT = Path.Join( os.environ[ 'VPROJECT' ], '../../content' )
  58.             return _CONTENT
  59.         except KeyError:
  60.             KeyError( '%VPROJECT% not defined' )
  61.  
  62. _PROJECT = None
  63. def project():
  64.     '''
  65.     returns a Path instance representing the %VPROJECT% path - path construction this way is super easy:
  66.     somePropPath = project() / 'models/props/some_prop.mdl'
  67.     '''
  68.     global _PROJECT
  69.     try:
  70.         _PROJECT = Path( os.environ[ 'VPROJECT' ] )
  71.         return _PROJECT
  72.     except KeyError:
  73.         raise KeyError( '%VPROJECT% not defined' )
  74.  
  75. _TOOLS = None
  76. def tools( engine='Source 2' ):
  77.     '''
  78.     returns the location of our tools.
  79.     '''
  80.     global _TOOLS
  81.    
  82.     if engine == 'Source':
  83.         if _TOOLS is None:
  84.             try:
  85.                 _TOOLS = Path(os.environ['VTOOLS'])
  86.             except KeyError:
  87.                 try:
  88.                     _TOOLS = Path.Join( os.environ[ 'VGAME' ], '/../../tools' )
  89.                    
  90.                 except KeyError:
  91.                     try:
  92.                         _TOOLS = Path.Join( os.environ[ 'VPROJECT' ], '/../../../tools' )
  93.                     except KeyError:
  94.                         raise KeyError('%VGAME% or %VPROJECT% not defined - cannot determine tools path')
  95.     else:
  96.         if _TOOLS is None:
  97.             try:
  98.                 _TOOLS = Path(os.environ['VTOOLS'])
  99.             except KeyError:
  100.                 try:
  101.                     _TOOLS = Path.Join( os.environ[ 'VGAME' ], '/sdktools' )
  102.                 except KeyError:
  103.                     try:
  104.                         _TOOLS = Path.Join( os.environ[ 'VPROJECT' ], '../sdktools' )
  105.                     except KeyError:
  106.                         raise KeyError('%VGAME% or %VPROJECT% not defined - cannot determine tools path')
  107.    
  108.     return _TOOLS
  109.  
  110. _PLATFORM = WIN_32_SUFFIX
  111. def platform():
  112.     '''
  113.     Returns the platform of the current environment, defaults to win32
  114.     '''
  115.     global _PLATFORM
  116.  
  117.     try:
  118.         _PLATFORM = os.environ[ 'VPLATFORM' ]
  119.     except KeyError:
  120.         try:
  121.             # next try to determine platform by looking for win64 bin directory in the path
  122.             bin64Dir = r'{0}\bin\{1}'.format( os.environ['VGAME'], WIN_64_SUFFIX )
  123.             if bin64Dir in os.environ['PATH']:
  124.                 _PLATFORM = WIN_64_SUFFIX
  125.         except ( KeyError, PathError ):
  126.             pass
  127.     return _PLATFORM
  128.  
  129. def iterContentDirectories():
  130.     for m in gameInfo.getSearchMods():
  131.         yield content() / m
  132.  
  133. def iterGameDirectories():
  134.     for m in gameInfo.getSearchMods():
  135.         yield game() / m
  136.  
  137. def resolveValvePath( valvePath, basePath=content() ):
  138.     '''
  139.     A "Valve Path" is one that is relative to a mod - in either the game or content tree.
  140.  
  141.     Ie: if you have a project with the content dir: d:/content and the game dir: d:/game
  142.     a "Valve Path" looks like this:  models/awesomeProps/coolThing.vmdl
  143.  
  144.     To resolve this path one must look under each mod the current project inherits from.
  145.     So a project "test" which inherits from "another" would result in the following searches:
  146.         d:/content/test/models/awesomeProps/coolThing.vmdl
  147.         d:/content/another/models/awesomeProps/coolThing.vmdl
  148.  
  149.     Similarly for the game tree.
  150.  
  151.     If the path cannot be resolved to a real file, None is returned.
  152.     '''
  153.     for mod in gameInfo.getSearchMods():
  154.         p = basePath / mod / valvePath
  155.         if p.exists:
  156.             return p
  157.  
  158. def setMod( newMod ):
  159.     '''
  160.     sets the current mod to something else.  makes sure to update VPROJECT, VMOD and re-parses global gameInfo for
  161.     the new mod so that calls to gamePath and contentPath return correctly
  162.     '''
  163.     global gameInfo
  164.     os.environ[ 'VMOD' ] = str( newMod )
  165.     os.environ[ 'VPROJECT' ] = (game() / newMod).asNative()
  166.     gameInfo = GameInfoFile()
  167.  
  168. def reportUsageToAuthor( author=None, payloadCB=None ):
  169.     '''
  170.     when called, this method will fire of a useage report email to whoever has marked themselves as the __author__ of the tool
  171.     the call was made from.  if no author is found then an email is sent to the DEFAULT_AUTHOR
  172.     '''
  173.     #additionalMsg = ''
  174.     #try:
  175.         #additionalMsg = payloadCB()
  176.     #except: pass
  177.  
  178.     #try:
  179.         #fr = inspect.currentframe()
  180.         #frameInfos = inspect.getouterframes( fr, 0 )
  181.  
  182.         #dataToSend = []
  183.         #if author is None:
  184.             ##set the default - in case we can't find an __author__ variable up the tree...
  185.             #author = DEFAULT_AUTHOR
  186.  
  187.         ##in this case, walk up the caller tree and find the top most __author__ variable definition
  188.         #for frameInfo in frameInfos:
  189.             #frame = frameInfo[0]
  190.             #dataToSend.append( '%s:  %s' % (Path( frameInfo[1] ) - '%VTOOLS%', frameInfo[3]) )
  191.  
  192.             #if author is None:
  193.                 #try:
  194.                     #author = frame.f_globals['__author__']
  195.                 #except KeyError: pass
  196.  
  197.                 #try:
  198.                     #author = frame.f_locals['__author__']
  199.                 #except KeyError: pass
  200.  
  201.         #import smtplib
  202.         #envDump = '\ncontent: %s\nproject: %s\n' % (content(), project())
  203.         #subject = '[using] %s' % str( Path( frameInfos[1][1] ).name() )
  204.         #msg = u'Subject: %s\n\n%s\n\n%s\n\n%s' % (subject, '\n'.join( map(str, dataToSend) ), envDump, additionalMsg)
  205.  
  206.         #def sendMail():
  207.             #try:
  208.                 #svr = smtplib.SMTP( MAIL_SERVER )
  209.                 #svr.sendmail( '%s@valvesoftware.com' % os.environ[ 'USERNAME' ], author, msg )
  210.             #except: pass
  211.  
  212.         ##throw the mail sending into a separate thread in case the mail server is being tardy - if it succeeds, great, if something happens, meh...
  213.         #threading.Thread( target=sendMail ).start()
  214.     ##NOTE: this method should never ever throw an exception...  its purely a useage tracking tool and if it fails, it should fail invisibly...
  215.     #except: pass
  216.  
  217.     pass
  218.  
  219. def asRelative( path ):
  220.     '''
  221.     '''
  222.     return str( Path(path).asRelative() )
  223.  
  224. def contentModRelativePath( path ):
  225.     '''
  226.     returns a path instance that is relative to the mod if the path is under the content tree
  227.     '''
  228.     return (path - content())[1:]
  229.  
  230. def projectRelativePath( path ):
  231.     '''
  232.     returns a path instance that is relative to vproject().  this method is provided purely for symmetry - its pretty trivial
  233.     '''
  234.     return path - project()
  235.  
  236. def makeSourceAbsolutePath( filepath ):
  237.     '''
  238.     Returns a Path instance as a "source" absolute filepath.
  239.     If the filepath doesn't exist under the project tree, the original filepath is returned.
  240.     '''
  241.     if filepath.isUnder( content() ):
  242.         relative = filepath - content()
  243.     elif filepath.isUnder( game() ):
  244.         relative = filepath - game()
  245.     else:
  246.         return filepath
  247.  
  248.     relToCurMod = relative[ 1: ]
  249.  
  250.     return relToCurMod
  251.  
  252. def makeSource1TexturePath( thePath ):
  253.     '''
  254.     returns the path as if it were a source1 texture/material path - ie the path is relative to the
  255.     materials, or materialsrc directory.  if the materials or materialsrc directory can't be found
  256.     in the path, the original path is returned
  257.     '''
  258.     if not isinstance( thePath, Path ):
  259.         thePath = Path( thePath )
  260.  
  261.     try:
  262.         idx = thePath.index( 'materials' )
  263.     except ValueError:
  264.         try:
  265.             idx = thePath.index( 'materialsrc' )
  266.         except ValueError:
  267.             return thePath
  268.  
  269.     return thePath[ idx+1: ]
  270.  
  271.  
  272. ########### DEPENDENCY CHECKING ###########
  273.  
  274. _VALIDATE_LOCATION_IMPORT_HOOK = None
  275.  
  276. def EnableValidDependencyCheck():
  277.     '''
  278.     sets up an import hook that ensures all imported modules live under the game directory
  279.     '''
  280.     global _VALIDATE_LOCATION_IMPORT_HOOK
  281.  
  282.     validPaths = [ game() ]
  283.     class Importer(object):
  284.         def find_module( self, fullname, path=None ):
  285.             lastName = fullname.rsplit( '.', 1 )[ -1 ]
  286.             scriptName = lastName +'.py'
  287.  
  288.             if path is None:
  289.                 path = []
  290.  
  291.             #not sure if there is a better way of asking python where it would look for a script/module - but I think this encapsulates the logic...
  292.             #at least under 2.6.  I think > 3 works differently?
  293.             for d in (path + sys.path):
  294.                 pyFilepath = Path( d ) / scriptName
  295.                 pyModulePath = Path( d ) / lastName / '__init__.py'
  296.                 if pyFilepath.exists or pyModulePath.exists:
  297.                     for validPath in validPaths:
  298.                         if pyFilepath.isUnder( validPath ):
  299.                             return None
  300.  
  301.                     print "### importing a script outside of game!", pyFilepath
  302.                     return None
  303.  
  304.             return None
  305.  
  306.     _VALIDATE_LOCATION_IMPORT_HOOK = Importer()
  307.     sys.meta_path.append( _VALIDATE_LOCATION_IMPORT_HOOK )
  308.  
  309. def DisableValidDependencyCheck():
  310.     '''
  311.     disables the location validation import hook
  312.     '''
  313.     global _VALIDATE_LOCATION_IMPORT_HOOK
  314.  
  315.     if _VALIDATE_LOCATION_IMPORT_HOOK is None:
  316.         return
  317.  
  318.     sys.meta_path.remove( _VALIDATE_LOCATION_IMPORT_HOOK )
  319.  
  320.     _VALIDATE_LOCATION_IMPORT_HOOK = None
  321.  
  322. try:
  323.     enableHook = os.environ[ 'ENSURE_PYTHON_CONTAINED' ]
  324.     if enableHook: EnableValidDependencyCheck()
  325. except KeyError:
  326.     pass
  327.  
  328.  
  329. ########### VALVE SPECIFIC PATH METHODS ###########
  330.  
  331. def expandAsGame( self, gameInfo, extension=None ):
  332.     '''
  333.     expands a given "mod" relative path to a real path under the game tree.  if an extension is not given, it is
  334.     assumed the path already contains an extension.  ie:  models/player/scout.vmt would get expanded to:
  335.     <gameRoot>/<mod found under>/models/player/scout.vmt - where the mod found under is the actual mod the file
  336.     exists under on the user's system
  337.     '''
  338.     thePath = self
  339.     if extension is not None:
  340.         thePath = self.setExtension( extension )
  341.  
  342.     searchPaths = gameInfo.getSearchPaths()
  343.     for path in searchPaths:
  344.         tmp = path / thePath
  345.         if tmp.exists:
  346.             return tmp
  347.  
  348.     return None
  349.  
  350. def expandAsContent( self, gameInfo, extension=None ):
  351.     '''
  352.     as for expandAsGame except for content rooted paths
  353.     '''
  354.     thePath = self
  355.     if extension is not None:
  356.         thePath = self.setExtension( extension )
  357.  
  358.     searchMods = gameInfo.getSearchMods()
  359.     for mod in searchMods:
  360.         underMod = '%VCONTENT%' / (mod / thePath)
  361.         if underMod.exists:
  362.             return underMod
  363.  
  364.     return None
  365.  
  366. def belongsToContent( self, gameInfo ):
  367.     for mod in gameInfo.getSearchMods():
  368.         if self.isUnder( content() / mod ):
  369.             return True
  370.  
  371.     return False
  372. def belongsToGame( self, gameInfo ):
  373.     for mod in gameInfo.getSearchMods():
  374.         if self.isUnder( game() / mod ):
  375.             return True
  376.     return False
  377.  
  378. def asRelative( self ):
  379.     '''
  380.     returns the path relative to either VCONTENT or VGAME.  if the path isn't under either of these directories
  381.     None is returned
  382.     '''
  383.     c = content()
  384.     g = game()
  385.     if self.isUnder( c ):
  386.         return self - c
  387.     elif self.isUnder( g ):
  388.         return self - g
  389.  
  390.     return None
  391.  
  392. Path.expandAsGame = expandAsGame
  393. Path.expandAsContent = expandAsContent
  394. Path.belongsToContent = belongsToContent
  395. Path.belongsToGame = belongsToGame
  396. Path.asRelative = asRelative
  397.  
  398.  
  399. ########### VALVE KEYVALUES PARSER ###########
  400.  
  401. def removeLineComments( lines ):
  402.     '''
  403.     removes all line comments from a list of lines
  404.     '''
  405.     newLines = []
  406.     for line in lines:
  407.         commentStart = line.find('//')
  408.         if commentStart != -1:
  409.             line = line[:commentStart]
  410.             if not line: continue
  411.             # strip trailing whitespace and tabs
  412.             line = line.rstrip( ' \t' )
  413.         newLines.append(line)
  414.  
  415.     return newLines
  416.  
  417. def removeBlockComments( lines ):
  418.     '''
  419.     removes all block comments from a list of lines
  420.     '''
  421.     newLines = []
  422.     end = len(lines)
  423.     n = 0
  424.     while n<end:
  425.         blockCommentStart = lines[n].find('/*')
  426.         newLines.append(lines[n])
  427.         contFlag = 0
  428.         if blockCommentStart != -1:
  429.             newLines[-1] = lines[n][:blockCommentStart]
  430.             while n<end:
  431.                 blockCommentEnd = lines[n].find('*/')
  432.                 if blockCommentEnd != -1:
  433.                     newLines[-1] += lines[n][blockCommentEnd+2:]
  434.                     n+=1
  435.                     contFlag = 1
  436.                     break
  437.                 n+=1
  438.         if contFlag: continue
  439.         n+=1
  440.     return newLines
  441.  
  442. class Chunk( object ):
  443.     '''
  444.     a chunk creates a reasonably convenient way to hold and access key value pairs, as well as a way to access
  445.     a chunk's parent.  the value attribute can contain either a string or a list containing other Chunk instances
  446.     '''
  447.     def __init__( self, key, value=None, parent=None, append=False, quoteCompoundKeys=True ):
  448.         self.key = key
  449.         self.value = value
  450.         self.parent = parent
  451.         self.quoteCompoundKeys = quoteCompoundKeys
  452.         if append:
  453.             parent.append(self)
  454.     def __getitem__( self, item ):
  455.         return self.value[item]
  456.     def __getattr__( self, attr ):
  457.         if self.hasLen:
  458.             for val in self.value:
  459.                 if val.key == attr:
  460.                     return val
  461.  
  462.         raise AttributeError( "has no attribute called %s"%attr )
  463.     def __len__( self ):
  464.         if self.hasLen: return len(self.value)
  465.         return None
  466.     def _hasLen( self ):
  467.         return isinstance( self.value, list )
  468.     hasLen = property(_hasLen)
  469.     def __iter__( self ):
  470.         if self.hasLen:
  471.             return iter(self.value)
  472.         raise TypeError( "non-compound value is not iterable" )
  473.     def __repr__( self, depth=0 ):
  474.         strLines = []
  475.  
  476.         compoundLine = '{0}{1}\n'
  477.         if self.quoteCompoundKeys:
  478.             compoundLine = '{0}"{1}"\n'
  479.  
  480.         if isinstance( self.value,list ):
  481.             strLines.append( compoundLine.format( '\t'*depth, self.key ) )
  482.             strLines.append( '\t'*depth +'{\n' )
  483.             for val in self.value: strLines.append( val.__repr__( depth+1 ) )
  484.             strLines.append( '\t'*depth +'}\n' )
  485.         else:
  486.             v = self.value
  487.  
  488.             strLines.append( '%s"%s" "%s"\n'%('\t'*depth, self.key, v) )
  489.  
  490.         return ''.join( strLines )
  491.     __str__ = __repr__
  492.     def __hash__( self ):
  493.         return id( self )
  494.     def iterChildren( self ):
  495.         '''
  496.         '''
  497.         if self.hasLen:
  498.             for chunk in self:
  499.                 if chunk.hasLen:
  500.                     for subChunk in chunk.iterChildren():
  501.                         yield subChunk
  502.                 else:
  503.                     yield chunk
  504.     def asDict( self, parentDict ):
  505.         if isinstance(self.value, list):
  506.             parentDict[self.key] = subDict = {}
  507.             for c in self.value:
  508.                 c.asDict(subDict)
  509.         else:
  510.             parentDict[self.key] = self.value
  511.     def append( self, new ):
  512.         '''
  513.         Append a chunk to the end of the list.
  514.         '''
  515.         if not isinstance( self.value, list ):
  516.             self.value = []
  517.  
  518.         self.value.append( new )
  519.  
  520.         #set the parent of the new Chunk to this instance
  521.         new.parent = self
  522.     def insert( self, index, new ):
  523.         '''
  524.         Insert a new chunk at a particular index.
  525.         '''
  526.         if not isinstance( self.value, list ):
  527.             self.value = []
  528.  
  529.         self.value.insert( index, new )
  530.    
  531.         # Set the parent of the new Chunk to this instance
  532.         new.parent = self      
  533.     def findKey( self, key ):
  534.         '''
  535.         recursively searches this chunk and its children and returns a list of chunks with the given key
  536.         '''
  537.         matches = []
  538.         if self.key == key:
  539.             matches.append(self)
  540.         if self.hasLen:
  541.             for val in self.value:
  542.                 matches.extend(val.findKey(key))
  543.  
  544.         return matches
  545.     def findValue( self, value ):
  546.         '''
  547.         recursively searches this chunk and its children and returns a list of chunks with the given value
  548.         '''
  549.         matches = []
  550.         if self.hasLen:
  551.             for val in self.value:
  552.                 matches.extend(val.findValue(value))
  553.         elif self.value == value:
  554.             matches.append(self)
  555.  
  556.         return matches
  557.     def findKeyValue( self, key, value ):
  558.         '''
  559.         recursively searches this chunk and its children and returns a list of chunks with the given key AND value
  560.         '''
  561.         matches = []
  562.         if self.hasLen:
  563.             for val in self.value:
  564.                 matches.extend(val.findKeyValue(key,value))
  565.         elif self.key == key and self.value == value:
  566.             matches.append(self)
  567.  
  568.         return matches
  569.     def testOnValues( self, valueTest ):
  570.         matches = []
  571.         if self.hasLen:
  572.             for val in self.value:
  573.                 matches.extend( val.testOnValues(valueTest) )
  574.         elif valueTest(self.value):
  575.             matches.append(self)
  576.  
  577.         return matches
  578.     def listAttr( self ):
  579.         #lists all the "attributes" - an attribute is just as a named key.  NOTE: only Chunks with length have attributes
  580.         attrs = []
  581.         for attr in self:
  582.             attrs.append(attr.key)
  583.  
  584.         return attrs
  585.     def hasAttr( self, attr ):
  586.         attrs = self.listAttr()
  587.         return attr in attrs
  588.     def getFileObject( self ):
  589.         '''
  590.         walks up the chunk hierarchy to find the file to which this chunk instance belongs
  591.         '''
  592.         parent = self.parent
  593.         lastParent = parent
  594.         safety = 1000
  595.         while parent is not None and safety:
  596.             lastParent = parent
  597.             parent = parent.parent
  598.             safety -= 1
  599.  
  600.         return lastParent
  601.  
  602.  
  603. def parseLine( line ):
  604.     '''
  605.     this line parser works well for all internal key value files I've thrown at it
  606.     '''
  607.     c = line.count('"')
  608.     if c == 4:
  609.         toks = line[1:-1].split('"')
  610.         return toks[0],toks[-1]
  611.     if c == 2:
  612.         #so this could mean we have a
  613.         #A) "key" value   OR
  614.         #B) key "value"   OR
  615.         #C) "key"
  616.         if line.strip().startswith('"'):
  617.             toks = [tok.strip() for tok in line[1:].split('"')]
  618.             tokCount = len(toks)
  619.             if tokCount == 2:
  620.                 return toks[0],toks[-1].strip()
  621.             elif tokCount == 1:
  622.                 return toks[0],[]
  623.         else:
  624.             toks = [tok.strip() for tok in line.rstrip()[:-1].split('"')]
  625.             #toks = line.split()
  626.             #idx = line.find('"')
  627.             #value = line[idx:].strip()[1:-1]
  628.             tokCount = len(toks)
  629.             if tokCount == 2:
  630.                 return toks[0],toks[-1].strip()
  631.             elif tokCount == 1:
  632.                 return toks[0],[]
  633.             elif tokCount > 2:
  634.                 key = toks[0]
  635.                 for v in toks[1:]:
  636.                     if v.strip() != '':
  637.                         return key, v
  638.     if c == 0:
  639.         toks = line.split()
  640.         tokCount = len(toks)
  641.         if tokCount == 2: return toks[0],toks[1]
  642.         if tokCount == 1: return toks[0],[]
  643.  
  644.  
  645. class KeyValueFile( object ):
  646.     '''
  647.     self.data contains a list which holds all the top level Chunk objects
  648.     '''
  649.     def __init__( self, filepath=None, lineParser=parseLine, chunkClass=Chunk, readCallback=None, supportsComments=True ):
  650.         '''
  651.         lineParser needs to return key,value
  652.         '''
  653.         self.filepath = filepath
  654.         self.data = self.value = []
  655.         self.key = None
  656.         self.parent = None
  657.         self.lineParser = lineParser
  658.         self.chunkClass = chunkClass
  659.         self.callback = readCallback
  660.         self.supportsComments = supportsComments
  661.  
  662.         def nullCallback(*args): pass
  663.         self.nullCallback = nullCallback
  664.  
  665.         #if no callback is defined, create a dummy one
  666.         if self.callback is None:
  667.             self.callback = nullCallback
  668.  
  669.         #if no line parser was given, then use a default one
  670.         if self.lineParser is None:
  671.             def simpleLineParse( line ):
  672.                 toks = line.split()
  673.                 if len(toks) == 1: return toks[0],[]
  674.                 else: return toks[0],toks[1]
  675.             self.lineParser = simpleLineParse
  676.  
  677.         #if a filepath exists, then read it
  678.         if self.filepath.exists:
  679.             self.read()
  680.     def getFilepath( self ):
  681.         return self._filepath
  682.     def setFilepath( self, newFilepath ):
  683.         '''
  684.         this wrapper is here so to ensure the _filepath attribute is a Path instance
  685.         '''
  686.         self._filepath = Path(newFilepath)
  687.     filepath = property(getFilepath, setFilepath)
  688.     def read( self, filepath=None ):
  689.         '''
  690.         reads the actual file, and passes the data read over to the parseLines method
  691.         '''
  692.         if filepath == None: filepath = self.filepath
  693.         else: filepath = Path(filepath)
  694.  
  695.         self.parseLines( filepath.read() )
  696.     def parseLines( self, lines ):
  697.         '''
  698.         this method does the actual parsing/data creation.  deals with comments, passing off data to the lineParser,
  699.         firing off the read callback, all that juicy stuff...
  700.         '''
  701.         lines = [l.strip() for l in lines]
  702.  
  703.         #remove comments
  704.         if self.supportsComments:
  705.             lines = removeLineComments(lines)
  706.             lines = removeBlockComments(lines)
  707.  
  708.         numLines = len(lines)
  709.  
  710.         #hold a list representation of the current spot in the hierarchy
  711.         parentList = [self]
  712.         parentListEnd = self
  713.         callback = self.callback
  714.         lineParser = self.lineParser
  715.         n = 0
  716.         for line in lines:
  717.             #run the callback - if there are any problems, replace the callback with the nullCallback
  718.             try: callback(n,numLines)
  719.             except: callback = self.nullCallback
  720.  
  721.             if line == '': pass
  722.             elif line == '{':
  723.                 curParent = parentList[-1][-1]
  724.                 parentList.append(curParent)
  725.                 parentListEnd = curParent
  726.             elif line == '}':
  727.                 parentList.pop()
  728.                 parentListEnd = parentList[-1]
  729.             else:
  730.                 try:
  731.                     key, value = lineParser(line)
  732.                 except TypeError:
  733.                     raise TypeError( 'Malformed kevalue found in file near line {0}'.format( n+1 ) )
  734.                 parentListEnd.append( self.chunkClass(key, value, parentListEnd) )
  735.             n += 1
  736.     def __getitem__( self, *args ):
  737.         '''
  738.         provides an index based way of accessing file data - self[0,1,2] accesses the third child of
  739.         the second child of the first root element in self
  740.         '''
  741.         args = args[0]
  742.         if not isinstance(args,tuple):
  743.             data = self.data[args]
  744.         else:
  745.             data = self.data[args[0]]
  746.             if len(args) > 1:
  747.                 for arg in args[1:]:
  748.                     data = data[arg]
  749.  
  750.         return data
  751.     def __len__( self ):
  752.         '''
  753.         lists the number of root elements in the file
  754.         '''
  755.         return len(self.data)
  756.     def __repr__( self ):
  757.         '''
  758.         this string representation of the file is almost identical to the formatting of a vmf file written
  759.         directly out of hammer
  760.         '''
  761.         strList = []
  762.         for chunk in self.data:
  763.             strList.append( str(chunk) )
  764.  
  765.         return ''.join(strList)
  766.     __str__ = __repr__
  767.     serialize = __repr__
  768.     def unserialize( self, theString ):
  769.         '''
  770.         '''
  771.         theStringLines = theString.split( '\n' )
  772.         self.parseLines( theStringLines )
  773.     @property
  774.     def hasLen( self ):
  775.         try:
  776.             self.data[ 0 ]
  777.             return True
  778.         except IndexError:
  779.             return False
  780.     def asDict( self ):
  781.         '''
  782.         returns a dictionary representing the key value file - this isn't always possible as it is valid for
  783.         a keyValueFile to have mutiple keys with the same key name within the same level - which obviously
  784.         isn't possible with a dictionary - so beware!
  785.         '''
  786.         asDict = {}
  787.         for chunk in self.data:
  788.             chunk.asDict( asDict )
  789.  
  790.         return asDict
  791.     def append( self, chunk ):
  792.         '''
  793.         appends data to the root level of this file - provided to make the vmf file object appear
  794.         more like a chunk object
  795.         '''
  796.         self.data.append( chunk )
  797.     def findKey( self, key ):
  798.         '''
  799.         returns a list of all chunks that contain the exact key given
  800.         '''
  801.         matches = []
  802.         for item in self.data:
  803.             matches.extend( item.findKey(key) )
  804.  
  805.         return matches
  806.     def findValue( self, value ):
  807.         '''
  808.         returns a list of all chunks that contain the exact value given
  809.         '''
  810.         matches = []
  811.         for item in self.data:
  812.             matches.extend( item.findValue(value) )
  813.  
  814.         return matches
  815.     def findKeyValue( self, key, value ):
  816.         '''
  817.         returns a list of all chunks that have the exact key and value given
  818.         '''
  819.         matches = []
  820.         for item in self.data:
  821.             matches.extend( item.findKeyValue(key,value) )
  822.  
  823.         return matches
  824.     def testOnValues( self, valueTest ):
  825.         '''
  826.         returns a list of chunks that return true to the method given - the method should take as its
  827.         first argument the value of the chunk it is testing against.  can be useful for finding values
  828.         containing substrings, or all compound chunks etc...
  829.         '''
  830.         matches = []
  831.         for item in self.data:
  832.             matches.extend( item.testOnValues(valueTest) )
  833.  
  834.         return matches
  835.     def write( self, filepath=None, doP4=True ):
  836.         '''
  837.         writes the instance back to disk - optionally to a different location from that which it was
  838.         loaded.  NOTE: deals with perforce should the file be managed by p4
  839.         '''
  840.         if filepath is None:
  841.             filepath = self.filepath
  842.         else:
  843.             filepath = Path(filepath)
  844.  
  845.         filepath.write(str(self), doP4=doP4)
  846.     def resetCache( self ):
  847.         pass
  848.  
  849.  
  850. class GameInfoFile( KeyValueFile ):
  851.     '''
  852.     Provides an interface to gameinfo relevant operations - querying search paths, game root, game title etc...
  853.     '''
  854.     def __init__( self, filepath=None, chunkClass=Chunk, readCallback=None ):
  855.  
  856.         try:
  857.             project()
  858.         except: return
  859.            
  860.         if filepath is None:
  861.             filepath = project() / 'gameinfo.gi'
  862.             # look for a gameinfo.txt instead, pick a gameinfo.gi as default if it doesn't exist.
  863.             if not filepath.exists:
  864.                 filepath = project() / 'gameinfo.txt'
  865.             if not filepath.exists:
  866.                 filepath = project() / 'gameinfo.gi'
  867.        
  868.         if filepath:
  869.             self.filename = filepath.split()[ - 1 ]
  870.         else:
  871.             self.filename = None
  872.  
  873.         KeyValueFile.__init__(self, filepath, parseLine, chunkClass, readCallback, True)
  874.     def __getattr__( self, attr ):
  875.         return getattr(self[0],attr)
  876.     def getSearchPaths( self ):
  877.         return [ Path.Join( '%VPROJECT%/../', mod ) for mod in self.getSearchMods() ]
  878.     def getSearchMods( self ):
  879.         #always has the base mod in it...
  880.         searchPaths = [mod()]
  881.         pathsChunk = self.FileSystem.SearchPaths
  882.         paths = [chunk.value for chunk in pathsChunk]
  883.         gi = '|gameinfo_path|'
  884.         sp = '|all_source_engine_paths|'
  885.         for path in paths:
  886.             pos = path.find(gi)
  887.             if pos != -1: continue
  888.  
  889.             path = path.replace(sp,'')
  890.             if not path: continue
  891.             if path not in searchPaths: searchPaths.append( path )
  892.  
  893.         return searchPaths
  894.     def getTitle( self ):
  895.         try:
  896.             return self[0].title.value
  897.         except AttributeError:
  898.             try:
  899.                 return self[0].game.value
  900.             except:
  901.                 return "unknown"
  902.     title = property(getTitle)
  903.     def getEngine( self ):
  904.         try:
  905.             return self[0].ToolsEnvironment.Engine.value
  906.         except AttributeError:
  907.             try:
  908.                 return self[0].engine.value
  909.             except:
  910.                 return "unknown"
  911.     engine = property( getEngine )
  912.     def getToolsDir( self ):
  913.         try:
  914.             return self[0].ToolsEnvironment.ToolsDir.value
  915.         except AttributeError:
  916.             try:
  917.                 return self[0].ToolsDir.value
  918.             except:
  919.                 return "unknown"
  920.     toolsDir = property( getToolsDir )
  921.     def writeDefaultFile( self ):
  922.         '''
  923.         Creates a default GameInfo file with basic structure
  924.         '''
  925.         self.filepath.write( '''"GameInfo"\n{\n\tgame "tmp"\n\tFileSystem\n\t{\n\t\tSearchPaths\n'+
  926.         '\t\t{\n\t\t\tGame |gameinfo_path|.\n\t\t}\n\t}\n}''' )
  927.     def simpleValidate( self ):
  928.         '''
  929.         Checks to see if the file has some basic keyvalues
  930.         '''
  931.         try:
  932.             getattr( self[0], 'game' )
  933.             getattr( self[0], 'SearchPaths' )
  934.             return True
  935.         except AttributeError:
  936.             raise GameInfoException( 'Not a valid gameinfo file.' )
  937.  
  938. # Read the current gameinfo
  939. gameInfo = GameInfoFile()
  940.  
  941. class GameInfoException(Exception):
  942.     pass
  943.  
  944.  
  945. def lsGamePath( path, recursive=False ):
  946.     '''
  947.     lists all files under a given 'valve path' - ie a game or content relative path.  this method needs to iterate
  948.     over all known search mods as defined in a project's gameInfo script
  949.     '''
  950.     path = Path( path )
  951.     files = []
  952.  
  953.     for modPath in [ base / path for base in gameInfo.getSearchPaths() ]:
  954.         files += list( modPath.files( recursive=recursive ) )
  955.  
  956.     return files
  957.  
  958.  
  959. def lsContentPath( path, recursive=False ):
  960.     '''
  961.     similar to lsGamePath except that it lists files under the content tree, not the game tree
  962.     '''
  963.     path = Path( path )
  964.     c = content()
  965.     files = []
  966.  
  967.     for modPath in [ c / mod / path for mod in gameInfo.getSearchMods() ]:
  968.         files += list( modPath.files( recursive=recursive ) )
  969.  
  970.     return files
  971.  
  972.  
  973. def contentPath( modRelativeContentPath ):
  974.     '''allows you do specify a path using mod relative syntax instead of a fullpath
  975.     example:
  976.        assuming vproject is set to d:/main/game/tf_movies
  977.        contentPath( 'models/player/soldier/parts/maya/soldier_reference.ma' )
  978.  
  979.        returns: d:/main/content/tf/models/player/soldier/parts/maya/soldier_reference.ma
  980.  
  981.     NOTE: None is returned in the file can't be found in the mod hierarchy
  982.     '''
  983.     return Path( modRelativeContentPath ).expandAsContent( gameInfo )
  984.  
  985.  
  986. def gamePath( modRelativeContentPath ):
  987.     '''allows you do specify a path using mod relative syntax instead of a fullpath
  988.     example:
  989.        assuming vproject is set to d:/main/game/tf
  990.        gamePath( 'models/player/soldier.mdl' )
  991.  
  992.        returns: d:/main/game/tf/models/player/soldier.mdl
  993.  
  994.     NOTE: None is returned in the file can't be found in the mod hierarchy
  995.     '''
  996.     return Path( modRelativeContentPath ).expandAsGame( gameInfo )
  997.  
  998.  
  999. def textureAsGameTexture( texturePath ):
  1000.     '''
  1001.     returns a resolved game texture filepath given some sort of texture path
  1002.     '''
  1003.     if not isinstance( texturePath, Path ):
  1004.         texturePath = Path( texturePath )
  1005.  
  1006.     if texturePath.isAbs():
  1007.         if texturePath.isUnder( content() ): relPath = texturePath - content()
  1008.         else: relPath = texturePath - game()
  1009.         relPath = relPath[ 2: ]
  1010.     else:
  1011.         relPath = texturePath
  1012.         if relPath.startswith( 'materials' ) or relPath.startswith( 'materialsrc' ):
  1013.             relPath = relPath[ 1: ]
  1014.  
  1015.     relPath = ('materials' / relPath).setExtension( 'vtf' )
  1016.     relPath = relPath.expandAsGame( gameInfo )
  1017.  
  1018.     return relPath
  1019.  
  1020.  
  1021. def textureAsContentTexture( texturePath ):
  1022.     '''
  1023.     returns a resolved content texture filepath for some sort of texture path.  it looks for psd
  1024.     first, and then for tga.  if neither are found None is returned
  1025.     '''
  1026.     if not isinstance( texturePath, Path ):
  1027.         texturePath = Path( texturePath )
  1028.  
  1029.     xtns = [ 'psd', 'tga' ]
  1030.     if texturePath.isAbs():
  1031.         if texturePath.isUnder( content() ): relPath = texturePath - content()
  1032.         else: relPath = texturePath - game()
  1033.         relPath = relPath[ 2: ]
  1034.     else:
  1035.         relPath = texturePath
  1036.         if relPath.startswith( 'materials' ) or relPath.startswith( 'materialsrc' ):
  1037.             relPath = relPath[ 1: ]
  1038.  
  1039.     contentPath = 'materialsrc' / relPath
  1040.     for xtn in xtns:
  1041.         tmp = contentPath.expandAsContent( gameInfo, xtn )
  1042.         if tmp is None: continue
  1043.  
  1044.         return tmp
  1045.  
  1046.     return None
  1047.  
  1048.  
  1049. def resolveMaterialPath( materialPath ):
  1050.     '''
  1051.     returns a resolved material path given some sort of material path
  1052.     '''
  1053.     if not isinstance( materialPath, Path ):
  1054.         materialPath = Path( materialPath )
  1055.  
  1056.     if materialPath.isAbs():
  1057.         if materialPath.isUnder( content() ): relPath = materialPath - content()
  1058.         else: relPath = materialPath - game()
  1059.         relPath = relPath[ 2: ]
  1060.     else:
  1061.         relPath = materialPath
  1062.         if relPath.startswith( 'materials' ) or relPath.startswith( 'materialsrc' ):
  1063.             relPath = relPath[ 1: ]
  1064.  
  1065.     relPath = ('materials' / relPath).setExtension( 'vmt' )
  1066.     relPath = relPath.expandAsGame( gameInfo )
  1067.  
  1068.     return relPath
  1069.  
  1070. #end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement