diff --git a/__init__.py b/__init__.py index 2179dd7..d086e1c 100644 --- a/__init__.py +++ b/__init__.py @@ -38,11 +38,13 @@ gui.EXPORT_OT_col, gui.EXPORT_OT_ipl, gui.EXPORT_OT_ide, + gui.EXPORT_OT_pawn, gui.SCENE_OT_dff_frame_move, gui.SCENE_OT_dff_atomic_move, gui.SCENE_OT_dff_update, gui.SCENE_OT_dff_import_map, gui.SCENE_OT_ipl_select, + gui.SCENE_OT_import_ide, gui.OBJECT_OT_dff_generate_bone_props, gui.OBJECT_OT_dff_set_parent_bone, gui.OBJECT_OT_dff_clear_parent_bone, diff --git a/gui/map_menus.py b/gui/map_menus.py index 1e55d28..4305d67 100644 --- a/gui/map_menus.py +++ b/gui/map_menus.py @@ -333,6 +333,70 @@ def atomics_active_changed(self, context): default = True ) +####################################################### +class DFFMapObjectProps(bpy.types.PropertyGroup): + + ipl_section: bpy.props.EnumProperty( + name="IPL Section", + items=(("inst", "inst", ""), ("cull", "cull", ""), ("enex", "enex", "")), + default="inst", + update=lambda self, ctx: ( + setattr(ctx.active_object, '["ipl_section"]', self.ipl_section) + if (ctx.active_object and self is ctx.active_object.dff_map) else None + ) + ) + + # IPL Data (Instance Placement) + object_id: bpy.props.IntProperty(name="Object ID", default=0) + model_name: bpy.props.StringProperty(name="Model Name", default="") + interior: bpy.props.IntProperty(name="Interior", default=0) + lod: bpy.props.IntProperty(name="LOD", default=0) + + # IDE Data (Object Definition) + ide_object_id: bpy.props.IntProperty(name="Object ID", default=0) + ide_model_name: bpy.props.StringProperty(name="Model Name", default="") + ide_txd_name: bpy.props.StringProperty(name="TXD Name", default="") + ide_draw_distance: bpy.props.IntProperty(name="Draw Distance", default=0) + ide_flags: bpy.props.IntProperty(name="Flags", default=0) + + ide_id: bpy.props.IntProperty(name="ID", default=0, + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_id",self.ide_id)) + ide_model: bpy.props.StringProperty(name="Model", default="", + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_model",self.ide_model)) + ide_txd: bpy.props.StringProperty(name="TXD", default="", + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_txd",self.ide_txd)) + ide_meshes: bpy.props.IntProperty(name="Mesh Count", default=1, min=1, + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_meshes",self.ide_meshes)) + ide_draw1: bpy.props.FloatProperty(name="DrawDist 1", default=0.0, min=0.0, + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_draw1",self.ide_draw1)) + ide_draw2: bpy.props.FloatProperty(name="DrawDist 2", default=0.0, min=0.0, + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_draw2",self.ide_draw2)) + ide_draw3: bpy.props.FloatProperty(name="DrawDist 3", default=0.0, min=0.0, + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_draw3",self.ide_draw3)) + ide_type: bpy.props.IntProperty(name="Line Type (1..4)", default=0, min=0, max=4, + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_type",self.ide_type)) + ide_section: bpy.props.EnumProperty( + name="Section", + items=(("objs","objs",""),("tobj","tobj",""),("anim","anim","")), + default="objs", + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_section",self.ide_section) + ) + ide_time_on: bpy.props.IntProperty(name="Time On", default=0, min=0, max=24, + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_time_on",self.ide_time_on)) + ide_time_off: bpy.props.IntProperty(name="Time Off", default=24, min=0, max=24, + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_time_off",self.ide_time_off)) + ide_anim: bpy.props.StringProperty(name="Anim", default="", + update=lambda self,ctx: DFFMapObjectProps._sync(self,ctx,"ide_anim",self.ide_anim)) + + pawn_model_name: bpy.props.StringProperty(name="Model Name", default="") + pawn_txd_name: bpy.props.StringProperty(name="Texture Name", default="") + + @staticmethod + def _sync(self_ref, context, key, value): + obj = context.active_object + if obj and hasattr(obj, "dff_map") and obj.dff_map is self_ref: + obj[key] = value + ####################################################### class MapImportPanel(bpy.types.Panel): """Creates a Panel in the scene context of the properties editor""" @@ -511,3 +575,135 @@ def draw(self, context): self.layout.operator(OBJECT_OT_dff_add_cull.bl_idname, text="CULL", icon="CUBE") self.layout.operator(OBJECT_OT_dff_add_grge.bl_idname, text="GRGE", icon="HOME") self.layout.operator(OBJECT_OT_dff_add_enex.bl_idname, text="ENEX", icon="OUTLINER_OB_MESH") + +####################################################### +class MapExportPanel(bpy.types.Panel): + """Creates a Panel in the scene context of the properties editor""" + bl_label = "DragonFF - Map Export" + bl_idname = "SCENE_PT_map_export" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "scene" + + ####################################################### + def draw(self, context): + layout = self.layout + settings = context.scene.dff + + flow = layout.grid_flow(row_major=True, + columns=0, + even_columns=True, + even_rows=False, + align=True) + + col = flow.column() + col.operator("scene.ide_export", text="Export IDE", icon='TEXT') + col.separator() + col.operator("export_scene.dff_ipl", text="Export IPL", icon='TEXT') + col.separator() + col.operator("scene.pwn_export", text="Export PWN", icon='TEXT') + +####################################################### +class MapProperties(bpy.types.Panel): + bl_label = "DragonFF - Map Properties" + bl_idname = "SCENE_PT_map_properties" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "scene" + + def draw(self, context): + layout = self.layout + obj = context.active_object + + if not obj or not hasattr(obj, "dff_map") or obj.dff_map is None: + layout.label(text="No map properties for this object.") + return + + props = obj.dff_map + + sec_cp = obj.get("ipl_section", None) + sec_pg = getattr(props, "ipl_section", None) + inferred = None + if hasattr(obj, "dff") and hasattr(obj.dff, "type"): + inferred = "cull" if obj.dff.type == "CULL" else ("inst" if obj.dff.type == "OBJ" else None) + section = sec_cp or sec_pg or inferred or "inst" + + header = layout.box().row(align=True) + header.label(text="IPL Section:") + header.prop(props, "ipl_section", text="") + + box = layout.box() + box.label(text="World Transform:") + col = box.column(align=True) + col.label(text=f"Position: {tuple(round(v, 3) for v in obj.location)}") + if hasattr(obj, "rotation_quaternion"): + r = obj.rotation_quaternion + col.label(text=f"Rotation (quat): {round(r[0],3)}, {round(r[1],3)}, {round(r[2],3)}, {round(r[3],3)}") + else: + e = obj.rotation_euler + col.label(text=f"Rotation: {tuple(round(v, 3) for v in e)}") + col.label(text=f"Scale: {tuple(round(v, 3) for v in obj.scale)}") + + if section == "inst": + + # IPL Data + box = layout.box() + box.label(text="IPL Data (INST):") + box.prop(props, "object_id", text="Object ID") + box.prop(props, "model_name", text="Model Name") + box.prop(props, "interior", text="Interior") + box.prop(props, "lod", text="LOD") + + # IDE + box = layout.box() + box.label(text="IDE Data (Object Definition):") + row = box.row(align=True) + row.prop(props, "ide_section", text="Section") + row.prop(props, "ide_flags", text="Flags") + col = box.column(align=True) + col.prop(props, "ide_object_id", text="Object ID") + col.prop(props, "ide_model_name", text="Model Name") + col.prop(props, "ide_txd_name", text="TXD Name") + col.prop(props, "ide_meshes", text="Mesh Count") + col.prop(props, "ide_draw1", text="DrawDist 1") + col.prop(props, "ide_draw2", text="DrawDist 2") + col.prop(props, "ide_draw3", text="DrawDist 3") + if props.ide_section == 'tobj': + col = box.column(align=True) + col.prop(props, "ide_time_on", text="Time On") + col.prop(props, "ide_time_off", text="Time Off") + elif props.ide_section == 'anim': + box.prop(props, "ide_anim", text="Anim Name") + + # Pawn + box = layout.box() + box.label(text="Pawn Data:") + box.prop(props, "pawn_model_name", text="Model Name") + box.prop(props, "pawn_txd_name", text="Texture Name") + + elif section == "cull": + box = layout.box() + box.label(text="CULL Zone:") + for key, label in ( + ("cull_center_x", "Center X"), + ("cull_center_y", "Center Y"), + ("cull_center_z", "Center Z"), + ("cull_radius", "Radius"), + ("cull_flags", "Flags"), + ("cull_min_x", "Min X"), + ("cull_min_y", "Min Y"), + ("cull_min_z", "Min Z"), + ("cull_max_x", "Max X"), + ("cull_max_y", "Max Y"), + ("cull_max_z", "Max Z"), + ): + if key in obj: + box.prop(obj, f'["{key}"]', text=label) + + else: + box = layout.box() + box.label(text=f"IPL Data ({section}):") + box.prop(props, "object_id", text="Object ID") + box.prop(props, "model_name", text="Model Name") + box.prop(props, "interior", text="Interior") + box.prop(props, "lod", text="LOD") \ No newline at end of file diff --git a/gui/map_ot.py b/gui/map_ot.py index 15e9fcf..ff0d2d6 100644 --- a/gui/map_ot.py +++ b/gui/map_ot.py @@ -19,6 +19,7 @@ import os import time +from bpy.props import StringProperty, CollectionProperty from bpy_extras.io_utils import ImportHelper, ExportHelper from ..ops import map_importer, ide_exporter, ipl_exporter @@ -310,13 +311,32 @@ class EXPORT_OT_ipl(bpy.types.Operator, ExportHelper): bl_label = "DragonFF IPL (.ipl)" filename_ext = ".ipl" - filepath : bpy.props.StringProperty(name="File path", - maxlen=1024, - default="", - subtype='FILE_PATH') + stream_distance: bpy.props.FloatProperty( + name="Stream Distance", + default=300.0, + description="Stream distance for dynamic objects" + ) + draw_distance: bpy.props.FloatProperty( + name="Draw Distance", + default=300.0, + description="Draw distance for objects" + ) + x_offset: bpy.props.FloatProperty( + name="X Offset", + default=0.0, + description="Offset for the x coordinate of the objects" + ) + y_offset: bpy.props.FloatProperty( + name="Y Offset", + default=0.0, + description="Offset for the y coordinate of the objects" + ) - filter_glob : bpy.props.StringProperty(default="*.ipl", - options={'HIDDEN'}) + z_offset: bpy.props.FloatProperty( + name="Z Offset", + default=0.0, + description="Offset for the z coordinate of the objects" + ) only_selected : bpy.props.BoolProperty( name = "Only Selected", @@ -353,8 +373,10 @@ def draw(self, context): settings = context.scene.dff layout = self.layout - layout.prop(self, "only_selected") + layout.prop(self, "x_offset") + layout.prop(self, "y_offset") + layout.prop(self, "z_offset") layout.prop(settings, "game_version_dropdown", text="Game") box = layout.box() @@ -403,6 +425,11 @@ def execute(self, context): return {'FINISHED'} + ####################################################### + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + ####################################################### class OBJECT_OT_dff_add_cull(bpy.types.Operator): @@ -558,3 +585,328 @@ def execute(self, context): obj.select_set(True) return {'FINISHED'} + +####################################################### +class SCENE_OT_import_ide(bpy.types.Operator): + """Import .IDE Files""" + bl_idname = "scene.ide_import" + bl_label = "Import IDE" + bl_options = {'REGISTER', 'UNDO'} + + files: CollectionProperty(type=bpy.types.PropertyGroup) + directory: StringProperty(subtype="DIR_PATH") + + filter_glob: StringProperty(default="*.ide", options={'HIDDEN'}) + + IDE_TO_SAMP_DL_IDS = {i: 0 + i for i in range(50000)} + + ####################################################### + def assign_ide_map_properties(self, obj, ide_data): + obj.dff_map.ide_object_id = ide_data.get("object_id", 0) + obj.dff_map.ide_model_name = ide_data.get("model_name", "") + obj.dff_map.ide_object_type = ide_data.get("object_type", "") + obj.dff_map.ide_txd_name = ide_data.get("txd_name", "") + obj.dff_map.ide_flags = ide_data.get("flags", 0) + obj.dff_map.ide_draw_distances = ide_data.get("draw_distances", "") + obj.dff_map.ide_draw_distance1 = ide_data.get("draw_distance1", 0.0) + obj.dff_map.ide_draw_distance2 = ide_data.get("draw_distance2", 0.0) + ####################################################### + def import_ide(self, filepaths, context): + for filepath in filepaths: + if not os.path.isfile(filepath): + print(f"File not found: {filepath}") + continue + + try: + with open(filepath, 'r', encoding='utf-8') as file: + lines = file.readlines() + except UnicodeDecodeError: + print(f"UTF-8 decoding failed for {filepath}, attempting ASCII decoding.") + try: + with open(filepath, 'r', encoding='ascii', errors='replace') as file: + lines = file.readlines() + except UnicodeDecodeError: + print(f"Error decoding file: {filepath}") + continue + + obj_data = {} + current = None # objs / tobj / anim / None + + def try_float(s): + try: return float(s) + except: return None + + for raw in lines: + line = raw.strip() + if not line or line.startswith("#"): + continue + + low = line.lower() + if low.startswith("objs"): + current = "objs"; continue + if low.startswith("tobj"): + current = "tobj"; continue + if low.startswith("anim"): + current = "anim"; continue + if low.startswith("end"): + current = None; continue + if not current: + continue + + parts = [p.strip() for p in line.split(",") if p.strip() != ""] + if len(parts) < 4: + print("Skipping short IDE line:", line) + continue + + try: + obj_id = int(parts[0]) + except: + print("Bad id on line:", line); continue + model = parts[1] + txd_name = parts[2] + + rec = { + "section": current, + "object_id": obj_id, + "model_name": model, + "txd_name": txd_name, + "mesh_count": None, + "draw_distances": [], + "flags": 0, + "time_on": None, + "time_off": None, + "anim_name": None, + } + + if current == "objs": + if len(parts) == 6: + rec["mesh_count"] = int(parts[3]) + rec["draw_distances"] = [try_float(parts[4]) or 0.0] + rec["flags"] = int(parts[5]) + elif len(parts) == 7: + rec["mesh_count"] = int(parts[3]) + rec["draw_distances"] = [try_float(parts[4]) or 0.0, + try_float(parts[5]) or 0.0] + rec["flags"] = int(parts[6]) + elif len(parts) == 8: + rec["mesh_count"] = int(parts[3]) + rec["draw_distances"] = [try_float(parts[4]) or 0.0, + try_float(parts[5]) or 0.0, + try_float(parts[6]) or 0.0] + rec["flags"] = int(parts[7]) + elif len(parts) == 5: + rec["mesh_count"] = None + rec["draw_distances"] = [try_float(parts[3]) or 0.0] + rec["flags"] = int(parts[4]) + else: + print("Unknown OBJS line format:", line) + continue + + elif current == "tobj": + # SA VC: Id,Model,TXD,Draw,TimeOn,TimeOff,Flags + if len(parts) != 7: + print("Unknown TOBJ line format:", line) + continue + rec["mesh_count"] = None + rec["draw_distances"] = [try_float(parts[3]) or 0.0] + rec["time_on"] = int(parts[4]) + rec["time_off"] = int(parts[5]) + rec["flags"] = int(parts[6]) + + elif current == "anim": + anim_name = parts[-1] + core = parts[:-1] + if len(core) == 6: + rec["mesh_count"] = int(core[3]) + rec["draw_distances"] = [try_float(core[4]) or 0.0] + rec["flags"] = int(core[5]) + elif len(core) == 7: + rec["mesh_count"] = int(core[3]) + rec["draw_distances"] = [try_float(core[4]) or 0.0, + try_float(core[5]) or 0.0] + rec["flags"] = int(core[6]) + elif len(core) == 8: + rec["mesh_count"] = int(core[3]) + rec["draw_distances"] = [try_float(core[4]) or 0.0, + try_float(core[5]) or 0.0, + try_float(core[6]) or 0.0] + rec["flags"] = int(core[7]) + elif len(core) == 5: + rec["mesh_count"] = None + rec["draw_distances"] = [try_float(core[3]) or 0.0] + rec["flags"] = int(core[4]) + else: + print("Unknown ANIM line format:", line) + continue + rec["anim_name"] = anim_name + + obj_data[model] = rec + + for obj in context.scene.objects: + base_name = obj.name.split('.')[0] + data = obj_data.get(base_name) + if not data or not hasattr(obj, "dff_map"): + continue + + props = obj.dff_map + + # IDE section + props.ide_section = data["section"] + props.ide_object_id = data["object_id"] + props.ide_model_name = data["model_name"] + props.ide_txd_name = data["txd_name"] + props.ide_flags = data["flags"] + + # Mesh count + draw distances + if data["mesh_count"] is not None: + props.ide_meshes = int(data["mesh_count"]) + dds = data["draw_distances"] + props.ide_draw1 = float(dds[0]) if len(dds) > 0 else 0.0 + props.ide_draw2 = float(dds[1]) if len(dds) > 1 else 0.0 + props.ide_draw3 = float(dds[2]) if len(dds) > 2 else 0.0 + + if data["section"] == "tobj": + props.ide_time_on = int(data["time_on"] or 0) + props.ide_time_off = int(data["time_off"] or 24) + elif data["section"] == "anim": + props.ide_anim = data["anim_name"] or "" + + props.object_id = data["object_id"] + props.model_name = data["model_name"] + + # Take Pawn Data from IDE unless already set + if not props.pawn_model_name: + props.pawn_model_name = data["model_name"] + if not props.pawn_txd_name: + props.pawn_txd_name = data["txd_name"] + + print(f"Assigned IDE properties to {obj.name}") + ####################################################### + def execute(self, context): + filepaths = [os.path.join(self.directory, f.name) for f in self.files] + self.import_ide(filepaths, context) + return {'FINISHED'} + ####################################################### + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} +####################################################### +class EXPORT_OT_ide(bpy.types.Operator, ExportHelper): + bl_idname = "scene.ide_export" + bl_label = "DragonFF IDE Export" + bl_description = "Export a GTA IDE file (objs/tobj/anim)" + filename_ext = ".ide" + + export_objs: bpy.props.BoolProperty(name="objs", default=True) + export_tobj: bpy.props.BoolProperty(name="tobj", default=False) + export_anim: bpy.props.BoolProperty(name="anim", default=False) + only_selected: bpy.props.BoolProperty(name="Only Selected", default=False) + + filter_glob: bpy.props.StringProperty(default="*.ide", options={'HIDDEN'}) + + ####################################################### + def draw(self, context): + layout = self.layout + col = layout.column(align=True) + col.prop(self, "export_objs") + col.prop(self, "export_tobj") + col.prop(self, "export_anim") + col.prop(self, "only_selected") + layout.prop(context.scene.dff, "game_version_dropdown", text="Game") + ####################################################### + def execute(self, context): + try: + map_exporter.export_ide({ + "file_name" : self.filepath, + "only_selected": self.only_selected, + "game_id" : context.scene.dff.game_version_dropdown, + "export_objs" : self.export_objs, + "export_tobj" : self.export_tobj, + "export_anim" : self.export_anim, + }) + + if not map_exporter.ide_exporter.total_definitions_num: + self.report({"ERROR"}, "No objects with IDE data found") + return {'CANCELLED'} + + return {'FINISHED'} + + except Exception as e: + self.report({"ERROR"}, str(e)) + return {'CANCELLED'} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} +####################################################### +class EXPORT_OT_pawn(bpy.types.Operator, ExportHelper): + bl_idname = "scene.pwn_export" + bl_label = "DragonFF Pawn Export" + bl_description = "Export Pawn for current scene" + filename_ext = ".pwn" + + only_selected: bpy.props.BoolProperty( + name="Only Selected", + default=False + ) + stream_distance: bpy.props.FloatProperty( + name="Stream Distance", + default=300.0 + ) + draw_distance: bpy.props.FloatProperty( + name="Draw Distance", + default=300.0 + ) + x_offset: bpy.props.FloatProperty( + name="X Offset", + default=0.0 + ) + y_offset: bpy.props.FloatProperty( + name="Y Offset", + default=0.0 + ) + + z_offset: bpy.props.FloatProperty( + name="Z Offset", + default=0.0, + description="Offset for the z coordinate of the objects" + ) + + filter_glob : bpy.props.StringProperty(default="*.pwn;*.inc", options={'HIDDEN'}) + ####################################################### + def draw(self, context): + layout = self.layout + layout.prop(self, "only_selected") + layout.prop(self, "stream_distance") + layout.prop(self, "draw_distance") + layout.prop(self, "x_offset") + layout.prop(self, "y_offset") + layout.prop(self, "z_offset") + layout.prop(context.scene.dff, "game_version_dropdown", text="Game") + ####################################################### + def execute(self, context): + try: + map_exporter.export_pawn({ + "file_name" : self.filepath, + "only_selected" : self.only_selected, + "game_id" : context.scene.dff.game_version_dropdown, + "stream_distance": self.stream_distance, + "draw_distance" : self.draw_distance, + "x_offset" : self.x_offset, + "y_offset" : self.y_offset, + "z_offset" : self.z_offset, + }) + + if not map_exporter.pwn_exporter.total_objects_num: + self.report({"ERROR"}, "No exportable meshes found") + return {'CANCELLED'} + return {'FINISHED'} + + except Exception as e: + self.report({"ERROR"}, str(e)) + return {'CANCELLED'} + ####################################################### + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} +####################################################### \ No newline at end of file diff --git a/ops/map_importer.py b/ops/map_importer.py index 637a1c6..7a038fe 100644 --- a/ops/map_importer.py +++ b/ops/map_importer.py @@ -14,8 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import bpy -import os + from ..gtaLib import map as map_utilites from ..ops import dff_importer, col_importer, txd_importer from .ipl.cull_importer import cull_importer @@ -42,6 +43,23 @@ class map_importer: map_section = "" settings = None + ####################################################### + @staticmethod + def fix_id(idblock): + if idblock is None: + return False + try: + _ = idblock.name + return True + except ReferenceError: + return False + + ####################################################### + def assign_map_properties(obj, ipl_data): + obj.dff_map.object_id = ipl_data.get("object_id", 0) + obj.dff_map.model_name = ipl_data.get("model_name", "") + obj.dff_map.interior = ipl_data.get("interior", 0) + obj.dff_map.lod = ipl_data.get("lod", 0) ####################################################### @staticmethod def import_object_instance(context, inst): @@ -71,6 +89,16 @@ def import_object_instance(context, inst): new_obj.rotation_quaternion = obj.rotation_quaternion new_obj.scale = obj.scale + try: + self.assign_map_properties(new_obj, { + "object_id": int(inst.id), + "model_name": str(model), + "interior": int(getattr(inst, "interior", 0)), + "lod": int(getattr(inst, "lod", -1)), + }) + except Exception: + pass + if not self.settings.create_backfaces: modifier = new_obj.modifiers.new("EdgeSplit", 'EDGE_SPLIT') # When added to some objects (empties?), returned modifier is None @@ -136,6 +164,8 @@ def import_object_instance(context, inst): new_obj.rotation_euler = obj.rotation_euler new_obj.scale = obj.scale + self.assign_map_properties(new_obj, inst) + if obj.parent: new_obj.parent = new_objects[obj.parent]