Package pyffi :: Package formats :: Package kfm
[hide private]
[frames] | no frames]

Source Code for Package pyffi.formats.kfm

  1  """ 
  2  :mod:`pyffi.formats.kfm` --- NetImmerse/Gamebryo Keyframe Motion (.kfm) 
  3  ======================================================================= 
  4   
  5  Implementation 
  6  -------------- 
  7   
  8  .. autoclass:: KfmFormat 
  9     :show-inheritance: 
 10     :members: 
 11   
 12  Regression tests 
 13  ---------------- 
 14   
 15  Read a KFM file 
 16  ^^^^^^^^^^^^^^^ 
 17   
 18  >>> # read kfm file 
 19  >>> stream = open('tests/kfm/test.kfm', 'rb') 
 20  >>> data = KfmFormat.Data() 
 21  >>> data.inspect(stream) 
 22  >>> print(data.nif_file_name.decode("ascii")) 
 23  Test.nif 
 24  >>> data.read(stream) 
 25  >>> stream.close() 
 26  >>> # get all animation file names 
 27  >>> for anim in data.animations: 
 28  ...     print(anim.kf_file_name.decode("ascii")) 
 29  Test_MD_Idle.kf 
 30  Test_MD_Run.kf 
 31  Test_MD_Walk.kf 
 32  Test_MD_Die.kf 
 33   
 34  Parse all KFM files in a directory tree 
 35  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
 36   
 37  >>> for stream, data in KfmFormat.walkData('tests/kfm'): 
 38  ...     print(stream.name) 
 39  tests/kfm/test.kfm 
 40   
 41  Create a KFM model from scratch and write to file 
 42  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
 43   
 44  >>> data = KfmFormat.Data() 
 45  >>> data.nif_file_name = "Test.nif" 
 46  >>> data.num_animations = 4 
 47  >>> data.animations.update_size() 
 48  >>> data.animations[0].kf_file_name = "Test_MD_Idle.kf" 
 49  >>> data.animations[1].kf_file_name = "Test_MD_Run.kf" 
 50  >>> data.animations[2].kf_file_name = "Test_MD_Walk.kf" 
 51  >>> data.animations[3].kf_file_name = "Test_MD_Die.kf" 
 52  >>> from tempfile import TemporaryFile 
 53  >>> stream = TemporaryFile() 
 54  >>> data.write(stream) 
 55  >>> stream.close() 
 56   
 57  Get list of versions and games 
 58  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
 59   
 60  >>> for vnum in sorted(KfmFormat.versions.values()): 
 61  ...     print('0x%08X' % vnum) 
 62  0x01000000 
 63  0x01024B00 
 64  0x0200000B 
 65  0x0201000B 
 66  0x0202000B 
 67  >>> for game, versions in sorted(KfmFormat.games.items(), 
 68  ...                              key=lambda x: x[0]): 
 69  ...     print("%s " % game + " ".join('0x%08X' % vnum for vnum in versions)) 
 70  Civilization IV 0x01000000 0x01024B00 0x0200000B 
 71  Emerge 0x0201000B 0x0202000B 
 72  Loki 0x01024B00 
 73  Megami Tensei: Imagine 0x0201000B 
 74  Oblivion 0x01024B00 
 75  Prison Tycoon 0x01024B00 
 76  Pro Cycling Manager 0x01024B00 
 77  Red Ocean 0x01024B00 
 78  Sid Meier's Railroads 0x0200000B 
 79  The Guild 2 0x01024B00 
 80  """ 
 81   
 82  # ***** BEGIN LICENSE BLOCK ***** 
 83  # 
 84  # Copyright (c) 2007-2011, NIF File Format Library and Tools. 
 85  # All rights reserved. 
 86  # 
 87  # Redistribution and use in source and binary forms, with or without 
 88  # modification, are permitted provided that the following conditions 
 89  # are met: 
 90  # 
 91  #    * Redistributions of source code must retain the above copyright 
 92  #      notice, this list of conditions and the following disclaimer. 
 93  # 
 94  #    * Redistributions in binary form must reproduce the above 
 95  #      copyright notice, this list of conditions and the following 
 96  #      disclaimer in the documentation and/or other materials provided 
 97  #      with the distribution. 
 98  # 
 99  #    * Neither the name of the NIF File Format Library and Tools 
100  #      project nor the names of its contributors may be used to endorse 
101  #      or promote products derived from this software without specific 
102  #      prior written permission. 
103  # 
104  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
105  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
106  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
107  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
108  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
109  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
110  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
111  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
112  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
113  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
114  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
115  # POSSIBILITY OF SUCH DAMAGE. 
116  # 
117  # ***** END LICENSE BLOCK ***** 
118   
119  import struct, os, re 
120   
121  import pyffi.object_models.xml 
122  import pyffi.object_models.common 
123  from pyffi.object_models.xml.basic import BasicBase 
124  from pyffi.utils.graph import EdgeFilter 
125  import pyffi.object_models 
126  import pyffi.object_models.xml.struct_ 
127 128 -class KfmFormat(pyffi.object_models.xml.FileFormat):
129 """This class implements the kfm file format.""" 130 xml_file_name = 'kfm.xml' 131 # where to look for kfm.xml and in what order: 132 # KFMXMLPATH env var, or KfmFormat module directory 133 xml_file_path = [os.getenv('KFMXMLPATH'), 134 os.path.join(os.path.dirname(__file__), "kfmxml")] 135 # file name regular expression match 136 RE_FILENAME = re.compile(r'^.*\.kfm$', re.IGNORECASE) 137 # used for comparing floats 138 _EPSILON = 0.0001 139 140 # basic types 141 int = pyffi.object_models.common.Int 142 uint = pyffi.object_models.common.UInt 143 byte = pyffi.object_models.common.UByte # not a typo 144 char = pyffi.object_models.common.Char 145 short = pyffi.object_models.common.Short 146 ushort = pyffi.object_models.common.UShort 147 float = pyffi.object_models.common.Float 148 SizedString = pyffi.object_models.common.SizedString 149 TextString = pyffi.object_models.common.UndecodedData # for text (used by older kfm versions) 150 151 # implementation of kfm-specific basic types 152
153 - class HeaderString(BasicBase):
154 """The kfm header string."""
155 - def __init__(self, **kwargs):
156 BasicBase.__init__(self, **kwargs) 157 self._doseol = False
158
159 - def __str__(self):
160 return ';Gamebryo KFM File Version x.x.x.x'
161
162 - def get_hash(self, data=None):
163 """Return a hash value for this value. 164 165 :return: An immutable object that can be used as a hash. 166 """ 167 return None
168
169 - def read(self, stream, data):
170 """Read header string from stream and check it. 171 172 :param stream: The stream to read from. 173 :type stream: file 174 :keyword version: The file version. 175 :type version: int 176 """ 177 # get the string we expect 178 version_string = self.version_string(data.version) 179 # read string from stream 180 hdrstr = stream.read(len(version_string)) 181 # check if the string is correct 182 if hdrstr != version_string.encode("ascii"): 183 raise ValueError( 184 "invalid KFM header: expected '%s' but got '%s'" 185 % (version_string, hdrstr)) 186 # check eol style 187 nextchar = stream.read(1) 188 if nextchar == '\x0d'.encode("ascii"): 189 nextchar = stream.read(1) 190 self._doseol = True 191 else: 192 self._doseol = False 193 if nextchar != '\x0a'.encode("ascii"): 194 raise ValueError( 195 "invalid KFM header: string does not end on \\n or \\r\\n")
196
197 - def write(self, stream, data):
198 """Write the header string to stream. 199 200 :param stream: The stream to write to. 201 :type stream: file 202 """ 203 # write the version string 204 stream.write(self.version_string(data.version).encode("ascii")) 205 # write \n (or \r\n for older versions) 206 if self._doseol: 207 stream.write('\x0d\x0a'.encode("ascii")) 208 else: 209 stream.write('\x0a'.encode("ascii"))
210
211 - def get_size(self, data=None):
212 """Return number of bytes the header string occupies in a file. 213 214 :return: Number of bytes. 215 """ 216 return len(self.version_string(data.version)) \ 217 + (1 if not self._doseol else 2)
218 219 # DetailNode 220
221 - def get_detail_display(self):
222 return str(self)
223 224 @staticmethod
225 - def version_string(version):
226 """Transforms version number into a version string. 227 228 :param version: The version number. 229 :type version: int 230 :return: A version string. 231 232 >>> KfmFormat.HeaderString.version_string(0x0202000b) 233 ';Gamebryo KFM File Version 2.2.0.0b' 234 >>> KfmFormat.HeaderString.version_string(0x01024b00) 235 ';Gamebryo KFM File Version 1.2.4b' 236 """ 237 if version == -1 or version is None: 238 raise RuntimeError('no string for version %s'%version) 239 return ";Gamebryo KFM File Version %s"%({ 240 0x01000000 : "1.0", 241 0x01024b00 : "1.2.4b", 242 0x0200000b : "2.0.0.0b", 243 0x0201000b : "2.1.0.0b", 244 0x0202000b : "2.2.0.0b" } [version])
245 246 # other types with internal implementation
247 - class FilePath(SizedString):
248 - def get_hash(self, data=None):
249 """Return a hash value for this value. 250 For file paths, the hash value is case insensitive. 251 252 :return: An immutable object that can be used as a hash. 253 """ 254 return self.get_value().lower()
255 256 @staticmethod
257 - def version_number(version_str):
258 """Converts version string into an integer. 259 260 :param version_str: The version string. 261 :type version_str: str 262 :return: A version integer. 263 264 >>> hex(KfmFormat.version_number('1.0')) 265 '0x1000000' 266 >>> hex(KfmFormat.version_number('1.2.4b')) 267 '0x1024b00' 268 >>> hex(KfmFormat.version_number('2.2.0.0b')) 269 '0x202000b' 270 """ 271 272 if not '.' in version_str: 273 return int(version_str) 274 275 try: 276 ver_list = [int(x, 16) for x in version_str.split('.')] 277 except ValueError: 278 # version not supported (i.e. version_str '10.0.1.3z' would 279 # trigger this) 280 return -1 281 if len(ver_list) > 4 or len(ver_list) < 1: 282 # version not supported 283 return -1 284 for ver_digit in ver_list: 285 if (ver_digit | 0xff) > 0xff: 286 return -1 # version not supported 287 while len(ver_list) < 4: 288 ver_list.append(0) 289 return ((ver_list[0] << 24) 290 + (ver_list[1] << 16) 291 + (ver_list[2] << 8) 292 + ver_list[3])
293
294 - class Header(pyffi.object_models.FileFormat.Data):
295 """A class to contain the actual kfm data.""" 296 version = 0x01024B00 297
298 - def inspect(self, stream):
299 """Quick heuristic check if stream contains KFM data, 300 by looking at the first 64 bytes. Sets version and reads 301 header string. 302 303 :param stream: The stream to inspect. 304 :type stream: file 305 """ 306 pos = stream.tell() 307 try: 308 hdrstr = stream.readline(64).rstrip() 309 finally: 310 stream.seek(pos) 311 if hdrstr.startswith(";Gamebryo KFM File Version ".encode("ascii")): 312 version_str = hdrstr[27:].decode("ascii") 313 else: 314 # not a kfm file 315 raise ValueError("Not a KFM file.") 316 try: 317 ver = KfmFormat.version_number(version_str) 318 except: 319 # version not supported 320 raise ValueError("KFM version not supported.") 321 if not ver in KfmFormat.versions.values(): 322 # unsupported version 323 raise ValueError("KFM version not supported.") 324 # store version 325 self.version = ver 326 # read header string 327 try: 328 self._header_string_value_.read(stream, self) 329 self._unknown_byte_value_.read(stream, self) 330 self._nif_file_name_value_.read(stream, self) 331 self._master_value_.read(stream, self) 332 finally: 333 stream.seek(pos)
334
335 - def read(self, stream):
336 """Read a kfm file. 337 338 :param stream: The stream from which to read. 339 :type stream: ``file`` 340 """ 341 # read the file 342 self.inspect(stream) # quick check 343 pyffi.object_models.xml.struct_.StructBase.read( 344 self, stream, self) 345 346 # check if we are at the end of the file 347 if stream.read(1): 348 raise ValueError('end of file not reached: corrupt kfm file?')
349
350 - def write(self, stream):
351 """Write a kfm file. 352 353 :param stream: The stream to which to write. 354 :type stream: ``file`` 355 """ 356 # write the file 357 pyffi.object_models.xml.struct_.StructBase.write( 358 self, stream, self)
359 360 # GlobalNode 361
362 - def get_global_child_nodes(self, edge_filter=EdgeFilter()):
363 return (anim for anim in self.animations)
364
365 - def get_global_display(self):
366 """Display the nif file name.""" 367 return self.nif_file_name
368
369 - class Animation:
370 # XXX this does not work yet (see todo for KfmFormat)
371 - def get_detail_display(self):
372 """Display the kf file name.""" 373 return self.kf_file_name if not self.name else self.name
374
375 - def get_global_display(self):
376 """Display the kf file name.""" 377 return self.kf_file_name if not self.name else self.name
378