# ***** 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 tools', 'description': 'Import-Export to VDrift track files', 'author': 'NaN, port of VDrift blender24 scripts', 'version': (0, 9), 'blender': (2, 6, 3), '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 from mathutils import Vector, Matrix class joe_vertex: bstruct = Struct(' 0: file.read(delta) joe = joe_obj().load(file) self.joe[name] = joe file.close() def save(self, filename): try: file = open(filename, 'rb+') except IOError: file = open(filename, 'wb') # header file.write(self.version) 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, obj in self.joe.items(): offset = file.tell() joe = joe_obj().from_mesh(obj) 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 not line.startswith('#') and '.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) # need to link an object to group to make it visible obj = bpy.data.objects.get('0') if not obj: obj = bpy.data.objects.new('0', None) grp.objects.link(obj) 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, texture): model = object.name self.values[0] = object.get('model', model) self.values[1] = object.get('texture', texture) 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 roads: @staticmethod def load(path): file = open(path, 'r') roadnum = int(file.readline()) file.readline() for i in range(roadnum): roads.load_road(file, 'road.' + str(i)) @staticmethod def save(path): file = open(path, 'w') meshes = [] i = 0 while 'road.' + str(i) in bpy.data.objects: obj = bpy.data.objects['road.' + str(i)] mesh = obj.data if obj.matrix_world != Matrix.Identity(4): mesh = obj.data.copy() mesh.transform(obj.matrix_world) meshes.append(mesh) i = i + 1 file.write(str(len(meshes)) + '\n\n') for m in meshes: roads.save_road(file, m) @staticmethod def load_road(file, name): patchnum = int(file.readline()) file.readline() # new mesh mesh = bpy.data.meshes.new(name) mesh.vertices.add(patchnum * 4 + 4) mesh.tessfaces.add(patchnum * 3) mesh.tessface_uv_textures.new() # parse road lines = [None] * 16 for p in range(patchnum): # road is stored reversed for n in range(15, -1, -1): lines[n] = file.readline() file.readline() # vertices first row, other rows are interpolated on export for n in range(4): i = p * 4 + n xyz = [float(s) for s in lines[n].split()] mesh.vertices[i].co = (xyz[2], xyz[0], xyz[1]) # faces for n in range(3): i = p * 3 + n vi = p * 4 + n mesh.tessfaces[i].vertices_raw = (vi, vi + 4, vi + 5, vi + 1) mesh.tessfaces[i].use_smooth = True u, v = 1 - n/3.0, float(p) mesh.tessface_uv_textures[0].data[i].uv_raw = (u, v, u, v + 1, u - 1/3.0, v + 1, u - 1/3.0, v) # last row for n in range(4): i = patchnum * 4 + n xyz = [float(s) for s in lines[n + 12].split()] mesh.vertices[i].co = (xyz[2], xyz[0], xyz[1]) # new object mesh.validate() mesh.update(calc_tessface = True) object = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(object) @staticmethod def save_road(file, mesh): if not mesh.tessfaces: mesh.calc_tessface() patchnum = int(len(mesh.vertices) / 4 - 1) #print('patches from facenum ' + str(len(mesh.tessfaces) / 3)) #print('patches from vertnum ' + str(patchnum)) road = [None] * 16 * patchnum if len(mesh.tessface_uv_textures) == 0 or len(mesh.tessface_uv_textures[0].data) == 0: raise NameError("Road mesh %s has no uv coordinates" % mesh.name) # get first, last patch rows from faces for i, f in enumerate(mesh.tessfaces): tf = mesh.tessface_uv_textures[0].data[i] for n in range(4): pointid = int(round(tf.uv_raw[2 * n] * 3)) patchid = int(round(tf.uv_raw[2 * n + 1])) id = patchid * 16 + pointid if patchid < patchnum: road[id] = mesh.vertices[f.vertices[n]].co if patchid > 0: road[id - 4] = mesh.vertices[f.vertices[n]].co # debug #for i, p in enumerate(road): # if p: # file.write(str(i) + ' %.4f %.4f %.4f\n' % (p[1], p[2], p[0])) # else: # file.write(str(i) + '\n') # calculate middle rows for i in range(patchnum - 1): roads.attach_patches(road, i, i + 1) # closed/open road if (road[0] - road[-1]).length < 1E-3: roads.attach_patches(road, -1, 0) else: roads.set_middlerow(road, 0, 1) roads.set_middlerow(road, -1, 2) # write road file.write(str(patchnum) + '\n\n') for i in range(patchnum): for n in range(3, -1, -1): for m in range(4): p = road[i * 16 + n * 4 + m] file.write('%.4f %.4f %.4f\n' % (p[1], p[2], p[0])) file.write('\n') # p0: first patch index # p1: second patch index @staticmethod def attach_patches(road, p0, p1): r0 = p0 * 16 r1 = p1 * 16 for n in range(4): i0 = r0 + n i1 = r1 + n slope = (road[i1 + 12] - road[i0]).normalized() len0 = (road[i0 + 12] - road[i0]).length len1 = (road[i1 + 12] - road[i1]).length scale = min(len1, len0) / 3.0 #old: (len1 + len0) / 6.0 road[i0 + 8] = road[i0 + 12] - slope * scale road[i1 + 4] = road[i1] + slope * scale # pi: patch index [0, patchnum) # ri: middle row index 1, 2 @staticmethod def set_middlerow(road, pi, ri): scale = ri / 3 for n in range(4): i = pi * 16 + n road[i + ri * 4] = road[i] + (road[i + 12] - road[i]) * scale class track: @staticmethod def load(path): start_position = {} obj = track.get_info() file = open(path, 'r') for line in file: line = line.rstrip('\n') if not line: continue name, value = line.split(' = ', 1) # generic properties (as strings for now) if name in obj: #if value == 'on' or value == 'yes': value = True #elif value == 'off' or value == 'no': value = False #ob[name] = type(ob[name])(value) obj[name] = value # lap sequences (as strings for now) elif name.startswith('lap sequence '): road, patch, unused = value.split(',', 2) obj[name] = road.split('.', 1)[0] + ':' + patch.split('.', 1)[0] # start positions elif name.startswith('start position '): x, y, z = value.split(',', 2) track.get_box(name).location = (float(z), float(x), float(y)) elif name.startswith('start orientation '): rad = 0.0174532925 x, y, z = value.split(',', 2) x, y, z = float(x) * rad, float(y) * rad, float(z) * rad name = 'start position ' + name.rsplit(' ', 1)[1] track.get_box(name).rotation_euler = (z, x, y) file.close() @staticmethod def save(path): file = open(path, 'w') obj = track.get_info() lap_sequence = [] for k, v in obj.items(): if k.startswith('lap sequence'): lap_sequence.append((k, v)) else: file.write(k + ' = ' + str(v) + '\n') file.write('lap sequences = ' + str(len(lap_sequence)) + '\n') for v in lap_sequence: name = v[0] road, patch = v[1].split(':', 1) file.write(name + ' = ' + road + ',' + patch + ',0\n') n = 0 while True: name = 'start position ' + str(n) obj = bpy.data.objects.get(name) if not obj: break x, y, z = obj.location file.write('start position %s = %.4f,%.4f,%.4f\n' % (str(n), y, z, x)) deg = 57.2957795 x, y, z = obj.rotation_euler x, y, z = x * deg, y * deg, z * deg file.write('start orientation %s = %.2f,%.2f,%.2f\n' % (str(n), y, z, x)) n = n + 1 file.close() @staticmethod def get_info(): obj = bpy.data.objects.get('track_info') if not obj: obj = bpy.data.objects.new('track_info', None) obj['cull faces'] = 'on' obj['vertical tracking skyboxes'] = 'no' obj['non-treaded friction coefficient'] = '1.0' obj['treaded friction coefficient'] = '0.9' bpy.context.scene.objects.link(obj) return obj; @staticmethod def get_box(name): obj = bpy.data.objects.get(name) if not obj: verts = [(1,-1,-1), (1,-1,1),(-1,-1,1),(-1,-1,-1),(1,1,-1),(1,1,1),(-1,1,1),(-1,1,-1)] edges = [(0,1),(1,2),(2,3),(3,7),(4,7),(5,6),(6,7),(0,3),(4,5),(1,5),(2,6),(0,4)] mesh = bpy.data.meshes.new("cube") mesh.from_pydata(verts, edges, []) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) obj.scale = (2.0, 0.9, 0.5) obj.show_axis = True return obj 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)) 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.tessfaces) == 0: object.data.calc_tessface() if len(object.data.tessfaces) == 0: raise NameError('Selected object has no faces!') if len(object.data.tessface_uv_textures) == 0: raise NameError('Selected object has no texture coordinates!') for mf in object.data.tessface_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.write(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.read(filepath) jpk.to_mesh() return {'FINISHED'} class import_joe_list(bpy.types.Operator, ImportHelper): bl_idname = 'import.list' bl_label = 'Import VDrift track objects' filename_ext = '.txt' filter_glob = StringProperty( default='*.txt', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) jpk = joe_pack.read(filepath) jpk.to_mesh() return {'FINISHED'} class export_trk(bpy.types.Operator, ExportHelper): bl_idname = 'export.trk' bl_label = 'Export Vdrift roads' filename_ext = '.trk' filter_glob = StringProperty( default='*.trk', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) roads.save(filepath) return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self); return {'RUNNING_MODAL'} class import_trk(bpy.types.Operator, ImportHelper): bl_idname = 'import.trk' bl_label = 'Import VDrift roads' filename_ext = '.trk' filter_glob = StringProperty( default='*.trk', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) roads.load(filepath) return {'FINISHED'} class export_track(bpy.types.Operator, ExportHelper): bl_idname = 'export.track' bl_label = 'Export Vdrift track' filename_ext = '.txt' filter_glob = StringProperty( default='track.txt', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) track.save(filepath) return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self); return {'RUNNING_MODAL'} class import_track(bpy.types.Operator, ImportHelper): bl_idname = 'import.track' bl_label = 'Import VDrift track' filename_ext = '.txt' filter_glob = StringProperty( default='track.txt', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) track.load(filepath) return {'FINISHED'} def read_vec(str): return tuple(float(i) for i in (str.split('#', 1)[0]).split(',')) def load_suspension(cfg, wheel_name): name = wheel_name+'.double-wishbone' if name in cfg: s = cfg[name] ucf = read_vec(s['upper-chassis-front']) ucr = read_vec(s['upper-chassis-rear']) uh = read_vec(s['upper-hub']) lcf = read_vec(s['lower-chassis-front']) lcr = read_vec(s['lower-chassis-rear']) lh = read_vec(s['lower-hub']) verts = [ucf, uh, ucr, lcf, lh, lcr] edges = [(0,1), (1,2), (2,0), (3,4), (4,5), (5,3), (1,4)] else: name = wheel_name+'.macpherson-strut' if name in cfg: s = cfg[name] e = read_vec(s['strut-end']) t = read_vec(s['strut-top']) h = read_vec(s['hinge']) verts = [h, e, t] edges = [(0,1), (1,2)] else: name = wheel_name+'.hinge' s = cfg[name] hw = read_vec(s['wheel']) hb = read_vec(s['chassis']) verts = [hw, hb] edges = [(0,1)] mesh = bpy.data.meshes.new(name) mesh.from_pydata(verts, edges, []) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) def load_wheel(cfg, wheel_name): tp = read_vec(cfg[wheel_name]['position']) ts = read_vec(cfg[wheel_name+'.tire']['size']) tw = ts[0] * 0.001 ta = ts[1] * 0.01 tr = ts[2] * 0.5 * 0.0254 + tw * ta bpy.ops.mesh.primitive_cylinder_add(vertices=16, radius=tr, depth=tw, location=tp, rotation=(0.0, 1.5708, 0.0)) load_suspension(cfg, wheel_name) class import_car(bpy.types.Operator, ImportHelper): bl_idname = 'import.car' bl_label = 'Import VDrift car' filename_ext = '.car' filter_glob = StringProperty( default='*.car', options={'HIDDEN'}) def execute(self, context): props = self.properties filepath = bpy.path.ensure_ext(self.filepath, self.filename_ext) try: from configparser import ConfigParser cfg = ConfigParser() cfg.read(filepath) load_wheel(cfg, 'wheel.fl') load_wheel(cfg, 'wheel.fr') load_wheel(cfg, 'wheel.rl') load_wheel(cfg, 'wheel.rr') filepath = path.join(path.dirname(filepath), 'body.joe') file = open(filepath, 'rb') joe = joe_obj().load(file) joe.to_mesh(bpy.path.basename(filepath), None) file.close() finally: self.report({'INFO'}, filepath + ' imported') 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 menu_import_joe_list(self, context): self.layout.operator(import_joe_list.bl_idname, text = 'VDrift Track Objects (list.txt)') def menu_export_trk(self, context): self.layout.operator(export_trk.bl_idname, text = 'VDrift Roads (.trk)') def menu_import_trk(self, context): self.layout.operator(import_trk.bl_idname, text = 'VDrift Roads (.trk)') def menu_export_track(self, context): self.layout.operator(export_track.bl_idname, text = 'VDrift Track Info (track.txt)') def menu_import_track(self, context): self.layout.operator(import_track.bl_idname, text = 'VDrift Track Info (track.txt)') def menu_import_car(self, context): self.layout.operator(import_car.bl_idname, text = 'VDrift Car (.car)') 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) bpy.types.INFO_MT_file_export.append(menu_export_trk) bpy.types.INFO_MT_file_import.append(menu_import_trk) bpy.types.INFO_MT_file_export.append(menu_export_track) bpy.types.INFO_MT_file_import.append(menu_import_track) bpy.types.INFO_MT_file_import.append(menu_import_joe_list) bpy.types.INFO_MT_file_import.append(menu_import_car) 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) bpy.types.INFO_MT_file_export.remove(menu_export_trk) bpy.types.INFO_MT_file_import.remove(menu_import_trk) bpy.types.INFO_MT_file_export.remove(menu_export_track) bpy.types.INFO_MT_file_import.remove(menu_import_track) bpy.types.INFO_MT_file_import.append(menu_import_joe_list) bpy.types.INFO_MT_file_import.append(menu_import_car) if __name__ == '__main__': register()