Skip to content

Instantly share code, notes, and snippets.

@mrwonko
Last active November 22, 2023 16:03
Show Gist options
  • Save mrwonko/3083023 to your computer and use it in GitHub Desktop.
Save mrwonko/3083023 to your computer and use it in GitHub Desktop.
Jedi Knight 2/3 .gla animation subrange extruder
#! /usr/bin/python
GLA_IDENT = b"2LGA"
GLA_VERSION = 6
import struct
class MdxaHeader:
def __init__( self ):
self.name = ""
self.scale = 1 # does not seem to be used by Jedi Academy anyway - or is it? I need it in import!
self.numFrames = -1
self.ofsFrames = -1
self.numBones = -1
self.ofsCompBonePool = -1
self.ofsSkel = -1 # this is also MdxaSkelOffsets.baseOffset + MdxaSkelOffsets.boneOffsets[0] - probably a historic leftover
self.ofsEnd = -1
def loadFromFile( self, file ):
# check ident
ident, = struct.unpack( "4s", file.read( 4 ) )
if ident != GLA_IDENT:
print( "File does not start with ", GLA_IDENT, " but ", ident, " - no GLA!" )
return False
version, = struct.unpack( "i", file.read( 4 ) )
if version != GLA_VERSION:
print( "Wrong gla file version! (" + str( version ) + " should be " + str( mrw_g2_constants.GLA_VERSION) + ")" )
return False
self.name = file.read( 64 )
self.scale, self.numFrames, self.ofsFrames, self.numBones, self.ofsCompBonePool, self.ofsSkel, self.ofsEnd = struct.unpack( "f6i", file.read( 7*4 ) )
return True
def saveToFile( self, file ):
file.write( struct.pack( "4si", GLA_IDENT, GLA_VERSION ) )
file.write( self.name )
file.write( struct.pack( "f6i", self.scale, self.numFrames, self.ofsFrames, self.numBones, self.ofsCompBonePool, self.ofsSkel, self.ofsEnd ) )
return True
def getSize( self ):
return 2*4 + 64*1 + 7*4
class MdxaBone:
def __init__( self ):
self.data = None
def loadFromFile( self, file ):
front = file.read( 64 + 4 + 4 + 12*4 + 12*4 )
rawNumChildren = file.read( 4 )
numChildren, = struct.unpack( "I", rawNumChildren )
self.data = front + rawNumChildren + file.read( 4 * numChildren )
return True
def saveToFile( self, file ):
file.write( self.data )
return True
def getSize( self ):
return len( self.data )
def ExtractSubRanges(filenameIn, filenameOut, frameRanges ):
# open file
file = open( filenameIn, "rb" )
# load header
header = MdxaHeader()
if not header.loadFromFile( file ):
return False
# load bone offsets (just after header)
boneOffsets = list(struct.unpack( "{}I".format( header.numBones ), file.read( 4 * header.numBones ) ))
# load bones
def loadBone( offset ):
file.seek( offset )
bone = MdxaBone()
assert( bone.loadFromFile( file ) )
return bone
bones = [ loadBone( header.getSize() + offset ) for offset in boneOffsets ]
# load requested frames, remembering the indices used
framesIn = []
numFramesOut = 0
for frameRange in frameRanges:
file.seek( header.ofsFrames + 3 * header.numBones * frameRange[0] )
framesIn.extend([ [ struct.unpack("I", file.read( 3 ) + b"\0" )[0] for _ in range( header.numBones ) ] for _ in range( frameRange[1] ) ])
numFramesOut += frameRange[1]
# load the requested indices from the compBonePool, building a bone index old-new lookup table
framesOut = []
indexLookup = {}
compBonePoolOut = []
for frame in framesIn:
frameOut = []
for index in frame:
indexOut = None
if index in indexLookup:
indexOut = indexLookup[index]
else:
indexOut = len(compBonePoolOut)
indexLookup[index] = indexOut
file.seek( header.ofsCompBonePool + 14 * index )
compBonePoolOut.append( file.read( 14 ) )
frameOut.append( indexOut )
framesOut.append( frameOut )
# change header
header.numFrames = numFramesOut
# calculate new offsets
header.ofsSkel = header.getSize() + header.numBones * 4
offset = header.numBones * 4
boneOffsets = []
for bone in bones:
boneOffsets.append( offset )
offset += bone.getSize()
offset += header.getSize()
header.ofsFrames = offset
offset += numFramesOut * header.numBones * 3
if offset % 4 != 0:
offset += 4 - (offset % 4)
header.ofsCompBonePool = offset
header.ofsEnd = offset + len( compBonePoolOut ) * 14
# open file
file = open(filenameOut, "wb")
# save new header
if not header.saveToFile( file ):
return False
# save bone offsets
for ofs in boneOffsets:
file.write( struct.pack( "I", ofs ) )
if file.tell() != header.getSize() + header.numBones * 4:
print( "Bug in script: bone offsets not written properly!" )
return False
# save bones
file.seek(header.ofsSkel)
for bone, offset in zip( bones, boneOffsets ):
if file.tell() != header.getSize() + offset:
print( "Bug in script: bone offset not correct!", file.tell(), header.getSize(), offset )
return False
if not bone.saveToFile( file ):
return False
# save frames
file.seek(header.ofsFrames)
for frame in framesOut:
for index in frame:
file.write( struct.pack( "I", index )[0:3] ) # only write first 3 bytes of each index
# save compBonePool
file.seek(header.ofsCompBonePool)
for compBone in compBonePoolOut:
file.write( compBone )
return True
import sys # contains sys.argv list
usage = "Usage:\n\
gla_range.py source.gla destination.gla startFrame frameCount\n\
gla_range.py source.gla destination.gla animation.cfg animationName [animationName, ...]"
if len( sys.argv ) < 5:
print( usage )
exit()
def isInt(var):
try:
int(var)
return True
except ValueError:
return False;
try:
# extract given range
ExtractSubRanges( sys.argv[1], sys.argv[2], [(int(sys.argv[3]), int( sys.argv[4] ))] )
except ValueError:
# extract given animation
# parse given animation.cfg
file = open( sys.argv[3] )
availableAnimations = {}
for line in file:
parts = line.split()
if len(parts) > 0 and parts[0][0:2] != "//":
try:
availableAnimations[parts[0].lower()] = ( int(parts[1]), int(parts[2]) )
except:
print( "Malformed line \"" + line + "\" in " + sys.argv[3] )
animations = []
for animName in sys.argv[4:]:
if animName.lower() not in availableAnimations:
print( "No \"" + animName + "\" animaton found in " + sys.argv[3] )
else:
animations.append( availableAnimations[animName.lower()] )
ExtractSubRanges( sys.argv[1], sys.argv[2], animations )
@mrwonko
Copy link
Author

mrwonko commented Jan 3, 2023

This script can extract individual frames from a Jedi Academy .gla animation file. The result can then be combined with other animations using the glamerge tool.

Run using Python.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment