# ***** BEGIN GPL LICENSE BLOCK ***** # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # ***** END GPL LICENCE BLOCK ***** bl_info = { 'name': 'VDrift JOE/JPK format', 'description': 'Import-Export to VDrift JOE files (.joe)', 'author': 'NaN, port of VDrift blender24 scripts', 'version': (0, 7), 'blender': (2, 5, 8), 'api': 35622, 'location': 'File > Import-Export', 'warning': '', 'wiki_url': 'http://', 'tracker_url': 'http://', 'category': 'Import-Export'} import bpy from bpy.props import StringProperty, BoolProperty from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.image_utils import load_image from struct import Struct from os import path class joe_vertex: bstruct = Struct(' 0: file.read(delta) joe = joe_obj().load(file) self.joe[name] = joe file.close() def save_jpk(self, filename): try: file = open(filename, 'rb+') except IOError: file = open(filename, 'wb') # header file.write(self.versionstr.encode('ascii')) data = joe_pack.bstruct.pack(self.numobjs, self.maxstrlen) file.write(data) # allocate fat fat_offset = file.tell() for i in range(self.numobjs): data = joe_pack.bstruct.pack(0, 0) file.write(data) name = util.fillz('', self.maxstrlen) file.write(name.encode('ascii')) # write data / build fat fat = [] for name, joe in self.joe.items(): offset = file.tell() joe.save(file) length = file.tell() - offset fat.append((offset, length, name)) # fill fat file.seek(fat_offset) for offset, length, name in fat: data = joe_pack.bstruct.pack(offset, length) file.write(data) name = util.fillz(name, self.maxstrlen) file.write(name.encode('ascii')) file.close() def load_list(self, filename): dir = path.dirname(filename) list_path = path.join(dir, 'list.txt') try: list_file = open(list_path) except IOError: print(list_path + ' not found.') return # read objects line = list_file.readline() while line != '': if '.joe' in line: object = trackobject() name = line.strip() line = object.read(name, list_file) self.list[object.values[0]] = object else: line = list_file.readline() if len(self.list) == 0: print('Failed to load list.txt.') list_file.close() def save_list(self, filename): dir = path.dirname(filename) list_path = path.join(dir, 'list.txt') file = open(list_path, 'w') file.write('17\n\n') i = 0 for name, object in self.list.items(): file.write('#entry ' + str(i) + '\n') object.write(file) i = i + 1 file.close() def load_images(self, filename): dir = path.dirname(filename) for name, object in self.list.items(): imagename = object.values[1] if imagename not in self.images: imagepath = path.join(dir, imagename) self.images[imagename] = load_image(imagepath) class trackobject: names = ('model', 'texture', 'mipmap', 'lighting', 'skybox', 'blend',\ 'bump length', 'bump amplitude', 'drivable', 'collidable',\ 'non treaded', 'treaded', 'roll resistance', 'roll drag',\ 'shadow', 'clamp', 'surface') namemap = dict(zip(names, range(17))) @staticmethod def create_groups(): trackobject.grp_surf = [] trackobject.grp = {} for name in ('mipmap', 'nolighting', 'skybox', 'transparent',\ 'doublesided', 'collidable', 'shadow', 'clampu', 'clampv'): grp = bpy.data.groups.get(name) if grp == None: grp = bpy.data.groups.new(name) trackobject.grp[name] = grp.objects @staticmethod def set_groups(): trackobject.create_groups() trackobject.is_surf = [] for grp in bpy.data.groups: if grp.name == 'mipmap': trackobject.is_mipmap = set(grp.objects) elif grp.name == 'nolighting': trackobject.is_nolighting = set(grp.objects) elif grp.name == 'skybox': trackobject.is_skybox = set(grp.objects) elif grp.name == 'transparent': trackobject.is_transparent = set(grp.objects) elif grp.name == 'doublesided': trackobject.is_doublesided = set(grp.objects) elif grp.name == 'collidable': trackobject.is_collidable = set(grp.objects) elif grp.name == 'shadow': trackobject.is_shadow = set(grp.objects) elif grp.name == 'clampu': trackobject.is_clampu = set(grp.objects) elif grp.name == 'clampv': trackobject.is_clampv = set(grp.objects) elif grp.name.startswith('surface'): trackobject.is_surf.append((grp.name.split('-')[-1], set(grp.objects))) def __init__(self): self.values = ['none', 'none', '1', '0', '0', '0',\ '1.0', '0.0', '0', '0',\ '1.0', '0.9', '1.0', '0.0',\ '0', '0', '0'] def read(self, name, list_file): i = 0 self.values[i] = name while True: line = list_file.readline() if line == '' or '.joe' in line: return line elif line.startswith('#') or line.startswith('\n'): continue else: i = i + 1 self.values[i] = line.strip() return line def write(self, list_file): for v in self.values: list_file.write(v + '\n') list_file.write('\n') def to_obj(self, object): object['model'] = self.values[0] object['texture'] = self.values[1] if self.values[2] == '1': trackobject.grp['mipmap'].link(object) if self.values[3] == '1': trackobject.grp['nolighting'].link(object) if self.values[4] == '1': trackobject.grp['skybox'].link(object) if self.values[5] == '1': trackobject.grp['transparent'].link(object) if self.values[5] == '2': trackobject.grp['doublesided'].link(object) if self.values[8] == '1' or self.values[9] == '1': trackobject.grp['collidable'].link(object) if self.values[14] == '1': trackobject.grp['shadow'].link(object) if self.values[15] == '1' or self.values[15] == '3': trackobject.grp['clampu'].link(object) if self.values[15] == '2' or self.values[15] == '3': trackobject.grp['clampv'].link(object) surfid = int(self.values[16]) while surfid >= len(trackobject.grp_surf): surfnum = len(trackobject.grp_surf) surfname = 'surface-'+str(surfnum) grp = bpy.data.groups.get(surfname) if grp == None: grp = bpy.data.groups.new(surfname) trackobject.grp_surf.append(grp.objects) trackobject.grp_surf[surfid].link(object) return self # set from object def from_obj(self, object): self.values[0] = object.get('model', object.name) self.values[1] = object.get('texture', object.data.uv_textures[0].data[0].image.name) self.values[2] = '1' if object in trackobject.is_mipmap else '0' self.values[3] = '1' if object in trackobject.is_nolighting else '0' self.values[4] = '1' if object in trackobject.is_skybox else '0' if object in trackobject.is_transparent: self.values[5] = '1' elif object in trackobject.is_doublesided: self.values[5] = '2' else: self.values[5] = '0' self.values[9] = '1' if object in trackobject.is_collidable else '0' self.values[14] = '1' if object in trackobject.is_shadow else '0' self.values[15] = '1' if object in trackobject.is_clampu else '0' if object in trackobject.is_clampv: self.values[15] = '2' if self.values[15] == '0' else '3' for name, grp in self.is_surf: if object in grp: self.values[16] = name break return self class util: # helper class to filter duplicates class indexed_set(object): def __init__(self): self.map = {} self.list = [] def get(self, ob): # using float as key in dict fixed = tuple(round(n, 5) for n in ob) if not fixed in self.map: ni = len(self.list) self.map[fixed] = ni self.list.append(fixed) else: ni = self.map[fixed] return ni # fill trailing zeroes @staticmethod def fillz(str, strlen): return str + chr(0)*(strlen - len(str)) @staticmethod def delete_object(object): bpy.context.scene.objects.unlink(object) bpy.data.objects.remove(object) @staticmethod def duplicate_object(object, name): # save current selection selected_objects = bpy.context.selected_objects[:] active_object = bpy.context.active_object bpy.ops.object.select_all(action = 'DESELECT') # copy object object.select = True bpy.ops.object.duplicate() object_duplicate = bpy.context.selected_objects[0] object_duplicate.name = name # reset selection bpy.context.scene.objects.active = active_object for obj in selected_objects: obj.select = True return object_duplicate @staticmethod def convert_to_tris(object): mesh = object.data bpy.context.scene.objects.active = object bpy.ops.object.mode_set(mode = 'EDIT', toggle = False) bpy.ops.mesh.select_all(action = 'SELECT') bpy.ops.mesh.quads_convert_to_tris() bpy.ops.object.mode_set(mode = 'OBJECT', toggle = False) return mesh @staticmethod def get_tri_mesh(object): quad = False mesh = object.data for face in mesh.faces: if len(face.vertices) == 4: quad = True break if quad: object = util.duplicate_object(object, '~joetmp') mesh = util.convert_to_tris(object) util.delete_object(object) return mesh class export_joe(bpy.types.Operator, ExportHelper): bl_idname = 'export.joe' bl_label = 'Export JOE' filename_ext = '.joe' filter_glob = StringProperty( default='*.joe', options={'HIDDEN'}) def __init__(self): try: self.object = bpy.context.selected_objects[0] except: self.object = None def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) if len(bpy.context.selected_objects[:]) != 1: raise NameError('Please select one object!') object = self.object if object.type != 'MESH': raise NameError('Selected object must be a mesh!') try: file = open(filepath, 'wb') joe = joe_obj().from_mesh(object) joe.save(file) file.close() finally: self.report({'INFO'}, object.name + ' exported') return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self); return {'RUNNING_MODAL'} class import_joe(bpy.types.Operator, ImportHelper): bl_idname = 'import.joe' bl_label = 'Import JOE' filename_ext = '.joe' filter_glob = StringProperty( default='*.joe', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) try: image = None #load_image(filepath_img) file = open(filepath, 'rb') joe = joe_obj().load(file) joe.to_mesh(bpy.path.basename(filepath), image) file.close() finally: self.report({'INFO'}, filepath + ' imported') return {'FINISHED'} class import_image(bpy.types.Operator, ImportHelper): bl_idname = 'import.image' bl_label = 'Import texture' filename_ext = '.png' filter_glob = StringProperty( default='*.png', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) image = load_image(filepath) if image == None: raise NameError('Failed to load image!') if len(bpy.context.selected_objects[:]) != 1: raise NameError('Please select one object!') object = bpy.context.selected_objects[0] if object.type != 'MESH': raise NameError('Selected object must be a mesh!') if len(object.data.faces) == 0: raise NameError('Selected object has no faces!') if len(object.data.uv_textures) == 0: raise NameError('Selected object has no texture coordinates!') for mf in object.data.uv_textures[0].data: mf.image = image return {'FINISHED'} class export_jpk(bpy.types.Operator, ExportHelper): bl_idname = 'export.jpk' bl_label = 'Export JPK' filename_ext = '.jpk' filter_glob = StringProperty( default='*.jpk', options={'HIDDEN'}) export_list = BoolProperty( name='Export properties (list.txt)', description='Export track objects properties', default=True) export_jpk = BoolProperty( name='Export objects (objects.jpk)', description='Export track objects as JPK', default=True) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) joe_pack.save(filepath, self.export_list, self.export_jpk) return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self); return {'RUNNING_MODAL'} class import_jpk(bpy.types.Operator, ImportHelper): bl_idname = 'import.jpk' bl_label = 'Import JPK' filename_ext = '.jpk' filter_glob = StringProperty( default='*.jpk', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) jpk = joe_pack.load(filepath) jpk.to_mesh() return {'FINISHED'} def menu_export_joe(self, context): self.layout.operator(export_joe.bl_idname, text = 'VDrift JOE (.joe)') def menu_import_joe(self, context): self.layout.operator(import_joe.bl_idname, text = 'VDrift JOE (.joe)') def menu_import_image(self, context): self.layout.operator(import_image.bl_idname, text = 'VDrift texture (.png)') def menu_export_jpk(self, context): self.layout.operator(export_jpk.bl_idname, text = 'VDrift JPK (.jpk)') def menu_import_jpk(self, context): self.layout.operator(import_jpk.bl_idname, text = 'VDrift JPK (.jpk)') def register(): bpy.utils.register_module(__name__) bpy.types.INFO_MT_file_export.append(menu_export_joe) bpy.types.INFO_MT_file_import.append(menu_import_joe) bpy.types.INFO_MT_file_import.append(menu_import_image) bpy.types.INFO_MT_file_export.append(menu_export_jpk) bpy.types.INFO_MT_file_import.append(menu_import_jpk) def unregister(): bpy.utils.unregister_module(__name__) bpy.types.INFO_MT_file_export.remove(menu_export_joe) bpy.types.INFO_MT_file_import.remove(menu_import_joe) bpy.types.INFO_MT_file_import.remove(menu_import_image) bpy.types.INFO_MT_file_export.remove(menu_export_jpk) bpy.types.INFO_MT_file_import.remove(menu_import_jpk) if __name__ == '__main__': register()