#!/usr/bin/python3 import collections, json, os, struct # Various constants from the game engine. These must be kept # synchronized with the game. NDL = 5 MAX_GUNS = 8 for defines in [ #common/main/robot.h """ #define N_ANIM_STATES 5 #define MAX_SUBMODELS 10 #define MAX_ROBOT_TYPES 85 // maximum number of robot types #define MAX_ROBOT_JOINTS 1600 """, #d2x-rebirth/main/weapon.h """ #define MAX_WEAPON_TYPES 70 """, #common/main/polyobj.h """ #define MAX_POLYGON_MODELS 200 """, #d2x-rebirth/main/bm.h """ #define MAX_OBJ_BITMAPS 610 """, #d2x-rebirth/main/bm.c """ #define N_D2_ROBOT_TYPES 66 #define N_D2_ROBOT_JOINTS 1145 #define N_D2_POLYGON_MODELS 166 #define N_D2_OBJBITMAPS 422 #define N_D2_OBJBITMAPPTRS 502 #define N_D2_WEAPON_TYPES 62 """, ]: for define in defines.strip().split('\n'): (_, name, value, *_) = define.split() globals()[name] = int(value) class SerializeableSingleType: class Instance: def __init__(self,descriptor,value): self.Struct = descriptor self.value = value def stwritephys(self,fp,**kwargs): s = self.Struct fp.write(s.pack(self.value)) def streadphys(self,fp,**kwargs): s = self.Struct return s.unpack_from(fp.read(s.size)) def streadjs(self,js,name,**kwargs): assert(name) return js class FixedIntegerArrayType(struct.Struct): class Instance: def __init__(self,array_descriptor,value): element_descriptor = array_descriptor.element_descriptor self.value = [element_descriptor.Instance(element_descriptor,v) for v in value] def stwritephys(self,fp): for value in self.value: value.stwritephys(fp=fp) def __init__(self,element_descriptor,element_count): self.element_descriptor = element_descriptor struct.Struct.__init__(self,b'<' + (element_descriptor.format[1:] * element_count)) self.Struct = self def streadphys(self,fp,**kwargs): return self.Instance(self,SerializeableSingleType.streadphys(self,fp)) def streadjs(self,js,name,**kwargs): return self.Instance(self,SerializeableSingleType.streadjs(self,js,name)) class IntegerType(struct.Struct): class Instance(SerializeableSingleType.Instance): def __init__(self,descriptor,value): SerializeableSingleType.Instance.__init__(self,descriptor,int(value)) def __init__(self,format_string): struct.Struct.__init__(self,format_string) self.Struct = self def __mul__(self,length): return type('array%u.%s' % (length, self.__class__.__name__), (FixedIntegerArrayType,), {})(self, length) def streadphys(self,fp,**kwargs): value = SerializeableSingleType.streadphys(self,fp) assert(len(value) == 1) return self.Instance(self,value[0]) def streadjs(self,js,name,**kwargs): return self.Instance(self,SerializeableSingleType.streadjs(self,js,name)) @classmethod def create(cls,name,format_string): return type(name, (cls,), {})(format_string) int16 = IntegerType.create('int16', '= self.maximum: raise MaximumLengthError("Invalid element count, must be less than %u, got %.8x; fp=%.8x" % (self.maximum, element_count, fp.tell())) for (field_name,field_type) in self.field: streadphys = field_type.streadphys container.value[field_name] = [streadphys(fp=fp,container=container,i=i) for i in range(element_count)] def streadjs(self,js,container,name,**kwargs): assert(name is None) element_count = None for (field_name,field_type) in self.field: value = js[field_name] lvalue = len(value) if element_count is not None and element_count != lvalue: raise MismatchLengthError("Invalid element count, must be %u to match prior list, but got %u" % (element_count, lvalue)) element_count = lvalue streadjs = field_type.streadjs container.value[field_name] = [streadjs(js=value[i],container=container,name=field_name) for i in range(element_count)] if element_count is None: if self.minimum > 0: raise MinimumLengthError("Invalid element count, must be at least %u, got None" % (self.minimum)) else: if element_count < self.minimum: raise MinimumLengthError("Invalid element count, must be at least %u, got %.8x" % (self.minimum, element_count)) if self.maximum is not None and element_count >= self.maximum: raise MaximumLengthError("Invalid element count, must be less than %u, got %.8x" % (self.maximum, element_count)) def stwritephys(self,fp,container): element_count = None for (field_name,field_type) in self.field: value = container.value[field_name] lvalue = len(value) if element_count is not None and element_count != lvalue: raise MismatchLengthError("Invalid element count, must be %u to match prior list, but got %u" % (element_count, lvalue)) element_count = lvalue if element_count is None: element_count = 0 fp.write(self.Struct.pack(element_count)) for (field_name,field_type) in self.field: for value in container.value[field_name]: value.stwritephys(fp=fp) class ByteBlobType: class Instance: def __init__(self,value): self.__value = value def stwritephys(self,fp): fp.write(self.__value) @property def value(self): return tuple(self.__value) def streadjs(self,js,**kwargs): return self.Instance(bytes(js)) class D2PolygonData(ByteBlobType): def streadphys(self,fp,i,container,**kwargs): size = container.value['polygon_models'][i].value[D2PolygonModel.model_data_size].value return self.Instance(fp.read(size)) class TailPaddingType(ByteBlobType): def streadphys(self,fp,**kwargs): return self.Instance(fp.read()) class PHYSFSX: Byte = uint8 Short = FixAng = int16 Int = Fix = int32 Vector = SerializeableStructType.create('PHYSFSX.Vector', ( ('x', int32), ('y', int32), ('z', int32), )) AngleVec = SerializeableStructType.create('PHYSFSX.AngleVec', ( ('p', int16), ('b', int16), ('h', int16), )) BitmapIndex = SerializeableStructType.create('BitmapIndex', ( ('index', PHYSFSX.Short), )) WeaponInfo = SerializeableStructType.create('WeaponInfo', ( ('render_type', PHYSFSX.Byte), ('persistent', PHYSFSX.Byte), ('model_num', PHYSFSX.Short), ('model_num_inner', PHYSFSX.Short), ('flash_vclip', PHYSFSX.Byte), ('robot_hit_vclip', PHYSFSX.Byte), ('flash_sound', PHYSFSX.Short), ('wall_hit_vclip', PHYSFSX.Byte), ('fire_count', PHYSFSX.Byte), ('robot_hit_sound', PHYSFSX.Short), ('ammo_usage', PHYSFSX.Byte), ('weapon_vclip', PHYSFSX.Byte), ('wall_hit_sound', PHYSFSX.Short), ('destroyable', PHYSFSX.Byte), ('matter', PHYSFSX.Byte), ('bounce', PHYSFSX.Byte), ('homing_flag', PHYSFSX.Byte), ('speedvar', PHYSFSX.Byte), ('flags', PHYSFSX.Byte), ('flash', PHYSFSX.Byte), ('afterburner_size', PHYSFSX.Byte), ('children', PHYSFSX.Byte), ('energy_usage', PHYSFSX.Fix), ('fire_wait', PHYSFSX.Fix), ('multi_damage_scale', PHYSFSX.Fix), ('bitmap', BitmapIndex), ('blob_size', PHYSFSX.Fix), ('flash_size', PHYSFSX.Fix), ('impact_size', PHYSFSX.Fix), ('strength', PHYSFSX.Fix * NDL), ('speed', PHYSFSX.Fix * NDL), ('mass', PHYSFSX.Fix), ('drag', PHYSFSX.Fix), ('thrust', PHYSFSX.Fix), ('po_len_to_width_ratio', PHYSFSX.Fix), ('light', PHYSFSX.Fix), ('lifetime', PHYSFSX.Fix), ('damage_radius', PHYSFSX.Fix), ('picture', BitmapIndex), ('hires_picture', BitmapIndex), )) Joint = SerializeableStructType.create('Joint', ( ('n_joints', PHYSFSX.Short), ('offset', PHYSFSX.Short), )) AnimationState = SerializeableStructType.create('AnimationState', ( ('joints', Joint * N_ANIM_STATES), )) D2RobotInfo = SerializeableStructType.create('D2RobotInfo', ( ('model_num', PHYSFSX.Int), ('gun_points', PHYSFSX.Vector * MAX_GUNS), ('gun_submodels', PHYSFSX.Byte * MAX_GUNS), ('exp1_vclip_num', PHYSFSX.Short), ('exp1_sound_num', PHYSFSX.Short), ('exp2_vclip_num', PHYSFSX.Short), ('exp2_sound_num', PHYSFSX.Short), ('weapon_type', PHYSFSX.Byte), ('weapon_type2', PHYSFSX.Byte), ('n_guns', PHYSFSX.Byte), ('contains_id', PHYSFSX.Byte), ('contains_count', PHYSFSX.Byte), ('contains_prob', PHYSFSX.Byte), ('contains_type', PHYSFSX.Byte), ('kamikaze', PHYSFSX.Byte), ('score_value', PHYSFSX.Short), ('badass', PHYSFSX.Byte), ('energy_drain', PHYSFSX.Byte), ('lighting', PHYSFSX.Fix), ('strength', PHYSFSX.Fix), ('mass', PHYSFSX.Fix), ('drag', PHYSFSX.Fix), ('field_of_view', PHYSFSX.Fix * NDL), ('firing_wait', PHYSFSX.Fix * NDL), ('firing_wait2', PHYSFSX.Fix * NDL), ('turn_time', PHYSFSX.Fix * NDL), ('max_speed', PHYSFSX.Fix * NDL), ('circle_distance', PHYSFSX.Fix * NDL), ('rapidfire_count', PHYSFSX.Byte * NDL), ('evade_speed', PHYSFSX.Byte * NDL), ('cloak_type', PHYSFSX.Byte), ('attack_type', PHYSFSX.Byte), ('see_sound', PHYSFSX.Byte), ('attack_sound', PHYSFSX.Byte), ('claw_sound', PHYSFSX.Byte), ('taunt_sound', PHYSFSX.Byte), ('boss_flag', PHYSFSX.Byte), ('companion', PHYSFSX.Byte), ('smart_blobs', PHYSFSX.Byte), ('energy_blobs', PHYSFSX.Byte), ('thief', PHYSFSX.Byte), ('pursuit', PHYSFSX.Byte), ('lightcast', PHYSFSX.Byte), ('death_roll', PHYSFSX.Byte), ('flags', PHYSFSX.Byte), ('pad', PHYSFSX.Byte * 3), ('deathroll_sound', PHYSFSX.Byte), ('glow', PHYSFSX.Byte), ('behavior', PHYSFSX.Byte), ('aim', PHYSFSX.Byte), ('anim_states', AnimationState * (MAX_GUNS + 1)), ('always_0xabcd', MagicNumberType(0xabcd)), )) D2RobotJoints = SerializeableStructType.create('D2RobotJoints', ( ('jointnum', PHYSFSX.Short), ('angles', PHYSFSX.AngleVec), )) D2PolygonModel = SerializeableStructType.create('D2PolygonModel', ( ('n_models', PHYSFSX.Int), ('model_data_size', PHYSFSX.Int), ('model_data_ptr', PHYSFSX.Int), ('submodel_ptrs', PHYSFSX.Int * MAX_SUBMODELS), ('submodel_offsets', PHYSFSX.Vector * MAX_SUBMODELS), ('submodel_norms', PHYSFSX.Vector * MAX_SUBMODELS), ('submodel_pnts', PHYSFSX.Vector * MAX_SUBMODELS), ('submodel_rads', PHYSFSX.Fix * MAX_SUBMODELS), ('submodel_parents', uint8 * MAX_SUBMODELS), ('submodel_mins', PHYSFSX.Vector * MAX_SUBMODELS), ('submodel_maxs', PHYSFSX.Vector * MAX_SUBMODELS), ('mins', PHYSFSX.Vector), ('maxs', PHYSFSX.Vector), ('rad', PHYSFSX.Fix), ('n_textures', PHYSFSX.Byte), ('first_texture', PHYSFSX.Short), ('simpler_model', PHYSFSX.Byte), )) D2PolygonModel.model_data_size = 'model_data_size' HAM1 = SerializeableStructType.create('HAM1', ( ('signature', MagicNumberType(1481130317)), # 'XHAM' ('version', uint32), (None, VariadicArray((('weapon_info', WeaponInfo),), 0, MAX_WEAPON_TYPES - N_D2_WEAPON_TYPES)), (None, VariadicArray((('robot_info', D2RobotInfo),), 0, MAX_ROBOT_TYPES - N_D2_ROBOT_TYPES)), (None, VariadicArray((('robot_joints', D2RobotJoints),), 0, MAX_ROBOT_JOINTS - N_D2_ROBOT_JOINTS)), (None, VariadicArray(( ('polygon_models', D2PolygonModel), ('polymodel_data', D2PolygonData()), ('dying_modelnum', int32), ('dead_modelnum', int32), ), 0, MAX_POLYGON_MODELS - N_D2_POLYGON_MODELS)), (None, VariadicArray(( ('obj_bitmaps', BitmapIndex), ), 0, MAX_OBJ_BITMAPS - N_D2_OBJBITMAPS)), (None, VariadicArray(( ('obj_bitmap_ptrs', int16), ), 0, MAX_OBJ_BITMAPS - N_D2_OBJBITMAPPTRS)), ('tail_padding', TailPaddingType()) )) class D2HXM1(SerializeableStructType): RobotTypeIndex = uint32 RobotJointIndex = uint32 PolygonIndex = uint32 ObjBitmapIndex = uint32 class D2HXMPolygonDataType(ByteBlobType): polygon_model = 'polygon_model' def streadphys(self,fp,container): size = container.value[self.polygon_model].value[D2PolygonModel.model_data_size].value return self.Instance(fp.read(size)) _struct_fields_ = ( ('signature', MagicNumberType(0x21584d48)), # '!MXH' ('version', uint32), (None, VariadicArray(( ('replace_robot', SerializeableStructType.create('ReplaceRobot', ( ('index', RobotTypeIndex), ('robot_info', D2RobotInfo), ))), ))), (None, VariadicArray(( ('replace_joint', SerializeableStructType.create('ReplaceJoint', ( ('index', RobotJointIndex), ('robot_joints', D2RobotJoints), ))), ))), (None, VariadicArray(( ('replace_polygon', SerializeableStructType.create('ReplacePolygon', ( ('index', PolygonIndex), (D2HXMPolygonDataType.polygon_model, D2PolygonModel), ('polymodel_data', D2HXMPolygonDataType()), ('dying_modelnum', int32), ('dead_modelnum', int32), ))), ))), (None, VariadicArray(( ('replace_objbitmap', SerializeableStructType.create('ReplaceObjBitmap', ( ('index', ObjBitmapIndex), ('obj_bitmap', BitmapIndex), ))), ))), (None, VariadicArray(( ('replace_objbitmapptr', SerializeableStructType.create('', ( ('index', ObjBitmapIndex), ('obj_bitmap_ptr', int16), ))), ))), ('tail_padding', TailPaddingType()) ) HXM1 = D2HXM1() class JSONEncoder(json.JSONEncoder): def default(self,o): try: return o.value except AttributeError: pass return json.JSONEncoder.default(self, o) class FileFormat: ExtensionTypeMap = { 'ham': HAM1, 'hxm': HXM1, } class UnknownFormatError(KeyError): pass class InvalidFormatError(KeyError): pass @classmethod def guess(cls,infile,outfile,arg): for filename in (infile, outfile): (b, ext) = os.path.splitext(filename) ext = ext.lower() if ext == '.json': ext = os.path.splitext(b)[1].lower() result = cls.ExtensionTypeMap.get(ext.lstrip('.')) if result: return result raise cls.UnknownFormatError("Unknown file format specify format using --%s" % arg) @classmethod def get(cls,infile,fmt,outfile,arg): inJSON = (os.path.splitext(infile)[1].lower().lstrip('.') == 'json') outJSON = (os.path.splitext(outfile)[1].lower().lstrip('.') == 'json') ecls = cls.ExtensionTypeMap[fmt] if fmt else FileFormat.guess(infile, outfile, arg) encodeToJSON = outJSON if inJSON ^ outJSON else None return (ecls, encodeToJSON) def main(): class IndentCheck: choices = ['tab', 'space', 'none'] class Iterator: def __init__(self,choices): self.index = 0 self.choices = choices def __iter__(self): return self def __next__(self): i = self.index self.index = i + 1 if i < len(self.choices): return self.choices[i] raise StopIteration def __contains__(self,value): return value.strip() == '' or value in self.choices def __iter__(self): return self.Iterator(self.choices) import argparse parser = argparse.ArgumentParser(description='convert Descent data files to/from binary/JSON') parser.add_argument('--format', choices=FileFormat.ExtensionTypeMap.keys(), help='binary file format to use', metavar='FORMAT') parser.add_argument('--indent', choices=IndentCheck(), help='how to indent JSON output', default='\t') group = parser.add_mutually_exclusive_group() group.add_argument('--encode', action='store_true', default=None, help='treat input as binary and output as JSON') group.add_argument('--decode', dest='encode', action='store_false', help='treat input as JSON and output as binary') parser.add_argument('input', help='file to read', metavar='input-file') parser.add_argument('output', help='file to write', metavar='output-file') args = parser.parse_args() (cls, encodeToJSON) = FileFormat.get(args.input,args.format,args.output,'format') if args.encode is not None: encodeToJSON = args.encode if encodeToJSON is None: parser.error("Neither input nor output ends in json. Use --encode or --decode to specify whether to produce JSON or consume JSON.") if encodeToJSON: with open(args.input, 'rb') as f: i = cls.streadphys(f) indent = args.indent if indent == 'space': indent = ' ' elif indent == 'tab': indent = '\t' elif indent == 'none': indent = '' with open(args.output, 'wt') as f: json.dump(i, f, cls=JSONEncoder, indent=indent) else: with open(args.input, 'rt') as f: i = cls.streadjs(js=json.load(f, object_pairs_hook=collections.OrderedDict)) with open(args.output, 'wb') as f: i.stwritephys(f) if __name__ == '__main__': main()