From c4745e38a1c8b31059485e66b529894913621cd9 Mon Sep 17 00:00:00 2001 From: Chris <120978532+spicybung@users.noreply.github.com> Date: Sun, 24 Aug 2025 01:16:04 -0400 Subject: [PATCH 1/6] format Inst lines, add map_exporter --- __init__.py | 10 +- gtaLib/map.py | 22 +- gtaLib/txd.py | 7 +- gui/dff_menus.py | 15 +- gui/map_menus.py | 252 ++++++++++++++- gui/map_ot.py | 652 +++++++++++++++++++++++++++++++++++-- ops/dff_exporter.py | 17 +- ops/map_exporter.py | 759 ++++++++++++++++++++++++++++++++++++++++++++ ops/map_importer.py | 310 +++++++++++++++++- 9 files changed, 2002 insertions(+), 42 deletions(-) create mode 100644 ops/map_exporter.py diff --git a/__init__.py b/__init__.py index c408672..3ceca68 100644 --- a/__init__.py +++ b/__init__.py @@ -35,12 +35,15 @@ gui.IMPORT_OT_dff, gui.EXPORT_OT_dff, gui.EXPORT_OT_col, - gui.EXPORT_OT_ipl_cull, + gui.EXPORT_OT_ipl, 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.EXPORT_OT_ide, + gui.EXPORT_OT_pawn, gui.OBJECT_OT_dff_generate_bone_props, gui.OBJECT_OT_dff_set_parent_bone, gui.OBJECT_OT_dff_clear_parent_bone, @@ -56,6 +59,8 @@ gui.OBJECT_OT_dff_add_2dfx_cover_point, gui.OBJECT_OT_dff_add_2dfx_escalator, gui.OBJECT_OT_dff_add_cull, + gui.OBJECT_OT_dff_add_garage, + gui.OBJECT_OT_dff_add_enex, gui.MATERIAL_PT_dffMaterials, gui.OBJECT_PT_dffObjects, gui.OBJECT_PT_dffCollections, @@ -69,9 +74,11 @@ gui.DFFObjectProps, gui.DFFCollectionProps, gui.MapImportPanel, + gui.MapProperties, gui.DFFFrameProps, gui.DFFAtomicProps, gui.DFFSceneProps, + gui.DFFMapObjectProps, gui.DFF_MT_ExportChoice, gui.DFF_MT_EditArmature, gui.DFF_MT_Pose, @@ -105,6 +112,7 @@ def register(): bpy.types.TextCurve.ext_2dfx = bpy.props.PointerProperty(type=gui.RoadSign2DFXObjectProps) bpy.types.Material.dff = bpy.props.PointerProperty(type=gui.DFFMaterialProps) bpy.types.Object.dff = bpy.props.PointerProperty(type=gui.DFFObjectProps) + bpy.types.Object.dff_map = bpy.props.PointerProperty(type=gui.DFFMapObjectProps) bpy.types.Collection.dff = bpy.props.PointerProperty(type=gui.DFFCollectionProps) bpy.types.TOPBAR_MT_file_import.append(gui.import_dff_func) diff --git a/gtaLib/map.py b/gtaLib/map.py index 3e5f106..aee3add 100644 --- a/gtaLib/map.py +++ b/gtaLib/map.py @@ -29,12 +29,16 @@ class MapData: object_instances: list object_data: dict cull_instances: list + garage_instances: list + enex_instances: list ####################################################### @dataclass class TextIPLData: object_instances: list cull_instances: list + garage_instances: list + enex_instances: list # Base for all IPL / IDE section reader / writer classes ####################################################### @@ -353,6 +357,8 @@ def load_map_data(game_id, game_root, ipl_section, is_custom_ipl): # Extract relevant sections object_instances = [] cull_instances = [] + garage_instances = [] + enex_instances = [] object_data = {} # Get all insts into a flat list (array) @@ -368,6 +374,14 @@ def load_map_data(game_id, game_root, ipl_section, is_custom_ipl): for entry in ipl['cull']: cull_instances.append(entry) + if 'grge' in ipl: + for entry in ipl['grge']: + garage_instances.append(entry) + + if 'enex' in ipl: + for entry in ipl['enex']: + enex_instances.append(entry) + # Get all objs and tobjs into flat ID keyed dictionaries if 'objs' in ide: for entry in ide['objs']: @@ -384,7 +398,9 @@ def load_map_data(game_id, game_root, ipl_section, is_custom_ipl): return MapData( object_instances = object_instances, object_data = object_data, - cull_instances = cull_instances + cull_instances = cull_instances, + garage_instances = garage_instances, + enex_instances = enex_instances ) ######################################################################## @@ -413,10 +429,10 @@ def write_text_ipl_to_stream(file_stream, game_id, ipl_data:TextIPLData): section_utility.write(file_stream, []) section_utility = SectionUtility("grge") - section_utility.write(file_stream, []) + section_utility.write(file_stream, ipl_data.garage_instances or []) section_utility = SectionUtility("enex") - section_utility.write(file_stream, []) + section_utility.write(file_stream, ipl_data.enex_instances or []) section_utility = SectionUtility("pick") section_utility.write(file_stream, []) diff --git a/gtaLib/txd.py b/gtaLib/txd.py index a257292..2e15414 100644 --- a/gtaLib/txd.py +++ b/gtaLib/txd.py @@ -636,8 +636,11 @@ def from_mem(data): ) = unpack_from("") + + for key, label in ( + ("grge_flag", "Door:"), + ("grge_type", "Garage:"), + ): + if key in obj: + grid.prop(obj, f'["{key}"]', text=label) + else: + row = grid.row(align=True) + row.label(text=f"{label} ") + + if "grge_name" in obj: + box.prop(obj, '["grge_name"]', text="Name") + else: + box.label(text="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 96a0f44..5eb8e2d 100644 --- a/gui/map_ot.py +++ b/gui/map_ot.py @@ -18,13 +18,17 @@ import math import os import time +import bmesh +from bpy.props import StringProperty, CollectionProperty from bpy_extras.io_utils import ImportHelper, ExportHelper -from ..ops import map_importer, ipl_exporter + +from ..ops import map_exporter, map_importer from ..ops.cull_importer import cull_importer from ..ops.importer_common import link_object + ####################################################### class SCENE_OT_dff_import_map(bpy.types.Operator): """Tooltip""" @@ -45,6 +49,9 @@ class SCENE_OT_dff_import_map(bpy.types.Operator): _cull_loaded = True + _grge_loaded = True + _enex_loaded = True + ####################################################### def modal(self, context, event): @@ -66,6 +73,26 @@ def modal(self, context, event): self._progress_current += 1 self._cull_loaded = True + # Import Garages if there are any left to load + elif not self._grge_loaded: + for g in getattr(importer, 'garage_instances', []): + try: + importer.import_garage(context, g) + except Exception as ex: + print("Can't import GRGE... skipping", ex) + self._progress_current += 1 + self._grge_loaded = True + + # Import Enex checkpoints if there are any left to load + elif not self._enex_loaded: + for e in self._importer.enex_instances: + try: + self._importer.import_enex(context, e) + except Exception as ex: + print("Can't import ENEX... skipping", ex) + self._progress_current += 1 + self._enex_loaded = True + # Import collision files if there are any left to load elif not self._col_loaded: num_objects_at_once = 5 @@ -144,6 +171,12 @@ def execute(self, context): else: self._cull_loaded = True + if self._importer.garage_instances: + self._grge_loaded = False + self._progress_total += 1 + else: + self._grge_loaded = True + if self._importer.col_files: self._col_index = 0 self._col_loaded = False @@ -151,6 +184,12 @@ def execute(self, context): else: self._col_loaded = True + if self._importer.enex_instances: + self._enex_loaded = False + self._progress_total += 1 + else: + self._enex_loaded = True + wm = context.window_manager wm.progress_begin(0, 100.0) @@ -201,60 +240,124 @@ def execute(self, context): return {'FINISHED'} ####################################################### -class EXPORT_OT_ipl_cull(bpy.types.Operator, ExportHelper): +class EXPORT_OT_ipl(bpy.types.Operator, ExportHelper): + bl_idname = "export_scene.dff_ipl" + bl_label = "DragonFF IPL Export" + bl_description = "Export a GTA IPL file with INST, CULL, or both sections" + filename_ext = ".ipl" - bl_idname = "export_scene.dff_ipl_cull" - bl_description = "Export a GTA CULL IPL File" - bl_label = "DragonFF CULL (.ipl)" - filename_ext = ".ipl" + export_inst: bpy.props.BoolProperty( + name="Export INST (object placements)", + default=True, + description="Export object placement (INST) section" + ) + export_cull: bpy.props.BoolProperty( + name="Export CULL zones", + default=True, + description="Export CULL (zone) section" + ) + + export_grge: bpy.props.BoolProperty( + name="Export GRGE zones", + default=True, + description="Export GRGE (zone) section" + ) + + export_enex: bpy.props.BoolProperty( + name="Export ENEX zones", + default=True, + description="Export ENEX (zone) section" + ) - filepath : bpy.props.StringProperty(name="File path", - maxlen=1024, - default="", - subtype='FILE_PATH') + only_selected: bpy.props.BoolProperty( + name="Only Selected", + default=False + ) + 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", - default = False + filter_glob: bpy.props.StringProperty( + default="*.ipl", + options={'HIDDEN'} ) ####################################################### def draw(self, context): layout = self.layout - + layout.prop(self, "export_inst") + layout.prop(self, "export_cull") + layout.prop(self, "export_grge") + layout.prop(self, "export_enex") layout.prop(self, "only_selected") + 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): - start = time.time() try: - ipl_exporter.export_ipl( + export_inst = self.export_inst + export_cull = self.export_cull + export_grge = self.export_grge + export_enex = self.export_enex + map_exporter.export_ipl( { - "file_name" : self.filepath, - "only_selected" : self.only_selected, - "game_id" : context.scene.dff.game_version_dropdown, - "export_inst" : False, - "export_cull" : True, + "file_name": self.filepath, + "only_selected": self.only_selected, + "game_id": context.scene.dff.game_version_dropdown, + "export_inst": export_inst, + "export_cull": export_cull, + "export_grge": export_grge, + "export_enex": export_enex, + "x_offset": self.x_offset, + "y_offset": self.y_offset, + "z_offset": self.z_offset, } ) - if not ipl_exporter.ipl_exporter.total_objects_num: - report = "No objects with IPL data found" - self.report({"ERROR"}, report) - return {'CANCELLED'}, report + if not map_exporter.ipl_exporter.total_objects_num: + self.report({"ERROR"}, "No objects with IPL data found") + return {'CANCELLED'} self.report({"INFO"}, f"Finished export in {time.time() - start:.2f}s") except Exception as e: self.report({"ERROR"}, str(e)) + return {'CANCELLED'} 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): @@ -308,3 +411,498 @@ def execute(self, context): obj.select_set(True) return {'FINISHED'} +####################################################### +class OBJECT_OT_dff_add_garage(bpy.types.Operator): + bl_idname = "object.dff_add_garage" + bl_label = "Add GRGE Zone" + bl_description = "Add a GRGE zone to the scene" + bl_options = {'REGISTER', 'UNDO'} + + location: bpy.props.FloatVectorProperty( + name="Location", + description="Location for the newly added garage sphere", + subtype='XYZ', + default=(0.0, 0.0, 0.0) + ) + + grge_type: bpy.props.IntProperty( + name="Garage Type", + description="Garage type ID", + default=5 + ) + + grge_flag: bpy.props.IntProperty( + name="Garage Flag", + description="Garage flag value", + default=0 + ) + + grge_name: bpy.props.StringProperty( + name="Garage Name", + description="Optional garage name", + default="Garage" + ) + + ####################################################### + def invoke(self, context, event): + self.location = context.scene.cursor.location + return self.execute(context) + + ####################################################### + def execute(self, context): + MapImporter = map_importer.map_importer + if getattr(MapImporter, "settings", None) is None: + MapImporter.settings = context.scene.dff + + # Always resolve the correct collection dynamically (III/VC/SA/etc.) + coll = MapImporter.create_grge_collection(context) + + me = MapImporter.create_grge_sphere() + obj = bpy.data.objects.new(f"GRGE_{self.grge_name}", me) + obj.location = self.location + obj.hide_render = True + + mat = bpy.data.materials.get("_GRGE") or bpy.data.materials.new("_GRGE") + mat.diffuse_color = (0.0, 0.35, 1.0, 1.0) + if not obj.data.materials: + obj.data.materials.append(mat) + else: + obj.data.materials[0] = mat + + obj.dff.type = "GRGE" + if hasattr(obj, "dff_map"): + obj.dff_map.ipl_section = "grge" + else: + obj["ipl_section"] = "grge" + + obj["grge_type"] = int(self.grge_type) + obj["grge_flag"] = int(self.grge_flag) + obj["grge_name"] = self.grge_name + + for k in ("grge_posX","grge_posY","grge_posZ","grge_lineX","grge_lineY", + "grge_cubeX","grge_cubeY","grge_cubeZ"): + if k not in obj: + obj[k] = 0.0 + + # Link exactly like add_cull does, but to the resolved GRGE collection + link_object(obj, coll) + + context.view_layer.objects.active = obj + for o in context.selected_objects: + o.select_set(False) + obj.select_set(True) + + return {'FINISHED'} +####################################################### +class OBJECT_OT_dff_add_enex(bpy.types.Operator): + bl_idname = "object.dff_add_enex" + bl_label = "Add ENEX Zone" + bl_description = "Add an ENEX (entry/exit) marker to the scene (wireframe cylinder, size 1.24)" + bl_options = {'REGISTER', 'UNDO'} + + location: bpy.props.FloatVectorProperty( + name="Location", + description="Location for the newly added ENEX marker", + subtype='XYZ', + default=(0.0, 0.0, 0.0) + ) + angle: bpy.props.FloatProperty( + name="Angle", + description="Angle around Z (radians)", + subtype='ANGLE', + default=0.0 + ) + name_hint: bpy.props.StringProperty( + name="Name", + description="Optional ENEX name", + default="ENEX" + ) + ####################################################### + def invoke(self, context, event): + self.location = context.scene.cursor.location + return self.execute(context) + ####################################################### + def execute(self, context): + MapImporter = map_importer.map_importer + if getattr(MapImporter, "settings", None) is None: + MapImporter.settings = context.scene.dff + + coll = MapImporter.create_enex_collection(context) + + try: + me = MapImporter.create_enex_cylinder() + except AttributeError: + me = bpy.data.meshes.new("_ENEX_") + bm = bmesh.new() + bmesh.ops.create_cone( + bm, segments=24, + radius1=1.24, radius2=1.24, depth=1.24, + cap_ends=True, cap_tris=False + ) + bm.to_mesh(me); bm.free() + + obj = bpy.data.objects.new( + f"ENEX_{self.name_hint}" if self.name_hint else "ENEX", me + ) + obj.location = self.location + obj.rotation_mode = 'ZXY' + obj.rotation_euler = (0.0, 0.0, float(self.angle)) + + obj.hide_render = True + try: + obj.display_type = 'WIRE' + except Exception: + pass + + mat = bpy.data.materials.get("_ENEX") or bpy.data.materials.new("_ENEX") + mat.diffuse_color = (1.0, 0.85, 0.10, 1.0) + if not obj.data.materials: + obj.data.materials.append(mat) + else: + obj.data.materials[0] = mat + + if hasattr(obj, "dff"): + obj.dff.type = "ENEX" + if hasattr(obj, "dff_map"): + obj.dff_map.ipl_section = "enex" + else: + obj["ipl_section"] = "enex" + + obj["enex_name"] = self.name_hint + obj["enex_posX"] = float(obj.location.x) + obj["enex_posY"] = float(obj.location.y) + obj["enex_posZ"] = float(obj.location.z) + obj["enex_rotZ"] = float(self.angle) + + link_object(obj, coll) + + context.view_layer.objects.active = obj + for o in context.selected_objects: + o.select_set(False) + 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/dff_exporter.py b/ops/dff_exporter.py index b92b6d5..a2dc1b4 100755 --- a/ops/dff_exporter.py +++ b/ops/dff_exporter.py @@ -598,14 +598,13 @@ def get_delta_morph_entries(obj, shape_keys): return dm_entries ####################################################### - @staticmethod def triangulate_mesh(mesh, preserve_loop_normals): loop_normals = [loop.normal.copy() for loop in mesh.loops] if preserve_loop_normals else [] # Check that the mesh is already triangulated if all(len(polygon.vertices) == 3 for polygon in mesh.polygons): return loop_normals - + bm = bmesh.new() bm.from_mesh(mesh) @@ -635,6 +634,8 @@ def triangulate_mesh(mesh, preserve_loop_normals): return loop_normals + + ####################################################### @staticmethod def find_vert_idx_by_tmp_idx(verts, idx): @@ -790,6 +791,7 @@ def populate_geometry_with_mesh_data(obj, geometry): normals = [vert.normal.copy() for vert in mesh.vertices] self.triangulate_mesh(mesh, False) + self.triangulate_mesh(mesh) # NOTE: Mesh.calc_normals is no longer needed and has been removed if bpy.app.version < (4, 0, 0): mesh.calc_normals() @@ -840,12 +842,19 @@ def populate_geometry_with_mesh_data(obj, geometry): for kb in shape_keys.key_blocks: sk_cos.append(kb.data[vert_index].co) - normal = normals[loop_index if use_loop_normals else vert_index] - + key = (vert_index, tuple(normal), tuple(tuple(uv) for uv in uvs)) + normal = normals[loop_index if use_loop_normals else vert_index] + + key = (loop.vertex_index, + tuple(loop.normal), + tuple(tuple(uv) for uv in uvs)) + + normal = loop.normal if obj.dff.export_split_normals else vertex.normal + if key not in verts_indices: face['verts'].append (len(vertices_list)) verts_indices[key] = len(vertices_list) diff --git a/ops/map_exporter.py b/ops/map_exporter.py new file mode 100644 index 0000000..c9383d3 --- /dev/null +++ b/ops/map_exporter.py @@ -0,0 +1,759 @@ +# GTA DragonFF - Blender scripts to edit basic GTA formats +# Copyright (C) 2019 Parik + +# 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 3 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, see . + +import os +import bpy + +from ..gtaLib.map import TextIPLData, MapDataUtility, SectionUtility +from ..gtaLib.data import map_data +from .cull_exporter import cull_exporter + + +####################################################### +def quat_xyzw(obj_or_empty): + """Quaternion (x,y,z,w).""" + q = getattr(obj_or_empty, "rotation_quaternion", None) + if q is None: + e = getattr(obj_or_empty, "rotation_euler", None) + q = (e.to_quaternion() if e is not None else None) + if q is None: + return 0.0, 0.0, 0.0, 1.0 + q = q.normalized() + return float(q.x), float(q.y), float(q.z), float(q.w) + +####################################################### +def euler_xyz(obj_or_empty): + """Euler XYZ in degrees.""" + e = getattr(obj_or_empty, "rotation_euler", None) + if e is None: + q = getattr(obj_or_empty, "rotation_quaternion", None) + e = (q.to_euler('XYZ') if q is not None else None) + if e is None: + return 0.0, 0.0, 0.0 + rad2deg = 180.0 / 3.141592653589793 + return float(e.x*rad2deg), float(e.y*rad2deg), float(e.z*rad2deg) +####################################################### +class ipl_exporter: + only_selected = False + game_id = None + export_inst = False + export_cull = False + export_grge = False + export_enex = False + + x_offset = 0.0 + y_offset = 0.0 + z_offset = 0.0 + + inst_objects = [] + cull_objects = [] + grge_objects = [] + enex_objects = [] + total_objects_num = 0 + ####################################################### + def _q(obj, key, default): + try: + return obj.get(key, default) + except Exception: + return default + ####################################################### + @staticmethod + def _anchor_and_pos(obj): + """Pick the transform carrier.""" + carrier = obj.parent if (obj.parent and obj.parent.type == 'EMPTY') else obj + loc = carrier.location + x = float(loc.x) + ipl_exporter.x_offset + y = float(loc.y) + ipl_exporter.y_offset + z = float(loc.z) + ipl_exporter.z_offset + return carrier, x, y, z + ####################################################### + @staticmethod + def _resolve_meta(obj): + """Read id/name/interior/lod.""" + dm = getattr(obj, "dff_map", None) + + # ID + object_id = ( + int(getattr(dm, "object_id", 0)) if dm else + int(obj.get("object_id", 0)) + ) + + # Model name + base_name = obj.name.split('.')[0] + model_name = ( + getattr(dm, "model_name", None) or + obj.get("model_name", None) or + base_name + ) + + # Interior + interior = ( + int(getattr(dm, "interior", 0)) if dm else + int(obj.get("interior", 0)) + ) + + # LOD index + if "LODIndex" in obj: + lod_index = int(obj["LODIndex"]) + else: + lod_index = int(getattr(dm, "lod", -1)) if dm else int(obj.get("lod", -1)) + + return object_id, model_name, interior, lod_index + ####################################################### + @staticmethod + def collect_objects(context): + self = ipl_exporter + self.inst_objects = [] + self.cull_objects = [] + self.grge_objects = [] + self.enex_objects = [] + + for obj in context.scene.objects: + if self.only_selected and not obj.select_get(): + continue + + dff_tag = getattr(obj, "dff", None) + dff_type = getattr(dff_tag, "type", None) + + # INST entries + if self.export_inst and obj.type == 'MESH' and dff_type == 'OBJ': + if self._skip_lod_or_col(obj, context): + continue + self.inst_objects.append(obj) + continue + + # CULL entries + if self.export_cull and obj.type == 'EMPTY' and dff_type == 'CULL': + self.cull_objects.append(obj) + continue + + # GRGE entries + if self.export_grge and obj.type == 'MESH' and dff_type == 'GRGE': + self.grge_objects.append(obj) + continue + + # ENEX entries + if self.export_enex and obj.type in {'MESH','EMPTY'} and dff_type == 'ENEX': + self.enex_objects.append(obj) + continue + + self.total_objects_num = len(self.inst_objects) + len(self.cull_objects) + len(self.grge_objects) + len(self.enex_objects) + + ####################################################### + @staticmethod + def _skip_lod_or_col(obj, context): + dff_scene = getattr(context.scene, "dff", None) + if dff_scene and getattr(dff_scene, "skip_lod", False): + if obj.name.startswith("LOD"): + return True + if ".ColMesh" in obj.name: + return True + return False + + ####################################################### + @staticmethod + def format_inst_line(obj, context=None): + ####################################################### + def round_float(v: float, places=10) -> str: + s = f"{v:.{places}f}".rstrip("0").rstrip(".") + return s if s else "0" + ####################################################### + def round_quat(v: float) -> str: + av = abs(v) + if av != 0.0 and av < 1e-7: + return f"{v:.10e}".replace("e-0", "e-").replace("e+0", "e+") + places = 9 if av >= 0.9 else 11 + return round_float(v, places) + ####################################################### + def format_inst_fields(v, places): + s = f"{v:.{places}f}" + if "." in s: + s = s.rstrip("0").rstrip(".") + if s in ("-0", "-0.0", "-0."): + s = "0" + return s + ####################################################### + if context is not None and ipl_exporter._skip_lod_or_col(obj, context): + return None + + carrier, x, y, z = ipl_exporter._anchor_and_pos(obj) + object_id, model_name, interior, lod_index = ipl_exporter._resolve_meta(obj) + + gid = ipl_exporter.game_id + + # fields + f6 = lambda v: format_inst_fields(v, 7) + f7 = lambda v: format_inst_fields(v, 7) + f10 = lambda v: format_inst_fields(v, 11) + f11 = lambda v: format_inst_fields(v, 11) + + if gid == map_data.game_version.SA: + qx, qy, qz, qw = quat_xyzw(carrier) + + qw_out = -qw + + return ( + f"{object_id}, {model_name}, {interior}, " + f"{round_float(x,6)}, {round_float(y,6)}, {round_float(z,6)}, " + f"{round_quat(qx)}, {round_quat(qy)}, " + f"{round_quat(qz)}, {round_quat(qw_out)}, " + f"{lod_index} # {obj.name}" + ) + + elif gid == map_data.game_version.VC: + sx, sy, sz = getattr(carrier, "scale", (1.0, 1.0, 1.0)) + qx, qy, qz, qw = quat_xyzw(carrier) + if qw < 0.0: + qx, qy, qz, qw = -qx, -qy, -qz, -qw + + return ( + f"{object_id}, {model_name}, {interior}, " + f"{f7(x)}, {f7(y)}, {f7(z)}, " + f"{f6(sx)}, {f6(sy)}, {f6(sz)}, " + f"{f11(qx)}, {f11(qy)}, {f11(qz)}, {f11(qw)}" + ) + + elif gid == map_data.game_version.III: + sx, sy, sz = getattr(carrier, "scale", (1.0, 1.0, 1.0)) + qx, qy, qz, qw = quat_xyzw(carrier) + if qw < 0.0: + qx, qy, qz, qw = -qx, -qy, -qz, -qw + + return ( + f"{object_id}, {model_name}, {interior}, " + f"{f7(x)}, {f7(y)}, {f7(z)}, " + f"{f6(sx)}, {f6(sy)}, {f6(sz)}, " + f"{f10(qx)}, {f10(qy)}, {f10(qz)}, {f10(qw)}" + ) + + else: + ex, ey, ez = euler_xyz(carrier) + return ( + f"{object_id}, {model_name}, " + f"{f6(x)}, {f6(y)}, {f6(z)}, " + f"{f6(ex)}, {f6(ey)}, {f6(ez)}, " + f"{lod_index} # {obj.name}" + ) + ####################################################### + @staticmethod + def format_grge_line(obj): + + # based on GTAMods + px = float(obj.get("grge_posX", obj.location.x)) + py = float(obj.get("grge_posY", obj.location.y)) + pz = float(obj.get("grge_posZ", obj.location.z)) + + lx = float(obj.get("grge_lineX", 0.0)) + ly = float(obj.get("grge_lineY", 0.0)) + + cx = float(obj.get("grge_cubeX", 0.0)) + cy = float(obj.get("grge_cubeY", 0.0)) + cz = float(obj.get("grge_cubeZ", 0.0)) + + gtype = int(obj.get("grge_type", 5)) + flag = int(obj.get("grge_flag", 0)) + + name = str(obj.get("grge_name", obj.name)) + + return (f"{px:.5f}, {py:.5f}, {pz:.5f}, " + f"{lx:.5f}, {ly:.5f}, " + f"{cx:.5f}, {cy:.5f}, {cz:.5f}, " + f"{flag}, {gtype}, {name}") + + ####################################################### + @staticmethod + def format_enex_line(obj): + ex = float(obj.get("enex_X1", obj.location.x)) + ey = float(obj.get("enex_Y1", obj.location.y)) + ez = float(obj.get("enex_Z1", obj.location.z)) + + p0 = float(obj.get("enex_EnterAngle", 0.0)) + + p1 = float(obj.get("enex_SizeX", 2.0)) + p2 = float(obj.get("enex_SizeY", 2.0)) + + p3 = int(obj.get("enex_SizeZ", obj.get("enex_Flags", 8))) + + tx = float(obj.get("enex_X2", ex)) + ty = float(obj.get("enex_Y2", ey)) + tz = float(obj.get("enex_Z2", ez)) + + ang = float(obj.get("enex_ExitAngle", 0.0)) + + interior = int(obj.get("enex_TargetInterior", obj.get("enex_interior", 0))) + mode = int(obj.get("enex_mode", 4)) + + name = obj.get("enex_Name", obj.name) + if name.startswith("ENEX_"): + name = name[5:] + name = f"\"{name}\"" + + t0 = int(obj.get("enex_Sky", 0)) + t1 = int(obj.get("enex_NumPedsToSpawn", 2)) + t2 = int(obj.get("enex_TimeOn", 0)) + t3 = int(obj.get("enex_TimeOff", 24)) + + def f5(v): return f"{v:.5f}" + def g(v): return f"{v:.10g}" + + return ( + f"{f5(ex)}, {f5(ey)}, {f5(ez)}, " + f"{g(p0)}, {g(p1)}, {g(p2)}, {p3}, " + f"{f5(tx)}, {f5(ty)}, {f5(tz)}, " + f"{g(ang)}, {interior}, {mode}, {name}, {t0}, {t1}, {t2}, {t3}" + ) + ####################################################### + def export_ipl(filename): + self = ipl_exporter + self.collect_objects(bpy.context) + if not self.total_objects_num and not (self.export_grge and getattr(self, "grge_objects", None)): + return + + # INST + object_instances = [] + for obj in self.inst_objects: + line = self.format_inst_line(obj, bpy.context) + if line: + object_instances.append(line) + + # CULL + cull_instances = cull_exporter.export_objects(self.cull_objects, self.game_id) + + # GRGE + garage_instances = [] + if self.export_grge and self.grge_objects: + for o in self.grge_objects: + s = self.format_grge_line(o) + if s: + garage_instances.append(s) + + # ENEX + enex_instances = [] + if self.export_enex and self.enex_objects: + for o in self.enex_objects: + s = self.format_enex_line(o) + if s: + enex_instances.append(s) + + # initialize + ipl_data = TextIPLData(object_instances, cull_instances, garage_instances, enex_instances) + MapDataUtility.write_ipl_data(filename, self.game_id, ipl_data) + +####################################################### +def export_ipl(options): + ipl_exporter.only_selected = bool(options.get('only_selected', False)) + ipl_exporter.game_id = options.get('game_id', None) + ipl_exporter.export_inst = bool(options.get('export_inst', True)) + ipl_exporter.export_cull = bool(options.get('export_cull', False)) + ipl_exporter.export_grge = bool(options.get('export_grge', False)) + ipl_exporter.export_enex = bool(options.get('export_enex', False)) + + ipl_exporter.x_offset = float(options.get('x_offset', 0.0)) + ipl_exporter.y_offset = float(options.get('y_offset', 0.0)) + ipl_exporter.z_offset = float(options.get('z_offset', 0.0)) + + ipl_exporter.export_ipl(options['file_name']) +####################################################### +class ide_exporter: + """Export an Item Definition file""" + only_selected = False + game_id = None + export_objs = True + export_tobj = False + export_anim = False + + objs = [] + tobj = [] + anim = [] + total_definitions_num = 0 + + ####################################################### + @staticmethod + def get_prop(obj, name, cast=None, fallback=None): + dff_map = getattr(obj, "dff_map", None) + val = None + if dff_map and hasattr(dff_map, name): + val = getattr(dff_map, name) + elif name in obj.keys(): + val = obj[name] + if val is None: + return fallback + try: + return cast(val) if cast else val + except Exception: + return fallback + + ####################################################### + @staticmethod + def skip_lod(obj, context): + dff_scene = getattr(context.scene, "dff", None) + return bool(dff_scene and getattr(dff_scene, "skip_lod", False) and obj.name.startswith("LOD")) + ####################################################### + @staticmethod + def is_exportable(obj, context): + if ide_exporter.only_selected and not obj.select_get(): + return False + if obj.type != "MESH": # skip empties, armatures, etc. + return False + if ".ColMesh" in obj.name: # skip collisions + return False + if obj.name != obj.name.split('.')[0]: + return False + dff_tag = getattr(obj, "dff", None) + if not (dff_tag and getattr(dff_tag, "type", None) == "OBJ"): + return False + if ide_exporter.skip_lod(obj, context): + return False + return True + + ####################################################### + @staticmethod + def draw_distances(obj): + s = ide_exporter.get_prop(obj, "ide_draw_distances", str) + if s: + vals = [v.strip() for v in s.split(",")] + return [float(v) for v in vals if v] + vals = [] + for k in ("ide_draw_distance", "ide_draw_distance1", "ide_draw_distance2", "ide_draw_distance3"): + v = ide_exporter.get_prop(obj, k, float) + if v is not None: + vals.append(v) + return vals or [100.0] + ####################################################### + @staticmethod + def count_obj_mesh_parts(root): + count = 0 + stack = [root] + seen = set() + while stack: + o = stack.pop() + if o in seen: + continue + seen.add(o) + if ( + o.type == "MESH" + and getattr(getattr(o, "dff", None), "type", None) == "OBJ" + and ".ColMesh" not in o.name + ): + count += 1 + stack.extend(list(o.children)) + return max(1, count) + + ####################################################### + @staticmethod + def collect_objs(context): + self = ide_exporter + self.objs, self.tobj, self.anim = [], [], [] + + for obj in context.scene.objects: + if not self.is_exportable(obj, context): + continue + section = (self.get_prop(obj, "ide_section", str, "objs") or "objs").lower() + if section == "objs" and self.export_objs: + self.objs.append(obj) + elif section == "tobj" and self.export_tobj: + self.tobj.append(obj) + elif section == "anim" and self.export_anim: + self.anim.append(obj) + + self.total_definitions_num = len(self.objs) + len(self.tobj) + len(self.anim) + + ####################################################### + @staticmethod + def fmt_objs(obj): + model_id = ide_exporter.get_prop(obj, "ide_object_id", int, + ide_exporter.get_prop(obj, "object_id", int)) + model_name = ide_exporter.get_prop(obj, "ide_model_name", str, + ide_exporter.get_prop(obj, "model_name", str, obj.name)) + txd_name = ide_exporter.get_prop(obj, "ide_txd_name", str, model_name) + flags = ide_exporter.get_prop(obj, "ide_flags", int, 0) + draws = ide_exporter.draw_distances(obj) + + if model_id is None or not model_name or not txd_name or not draws: + return None + + # Get type + t = ide_exporter.get_prop(obj, "ide_type", int, 0) + if t not in (1, 2, 3, 4): + gid = str(ide_exporter.game_id or "").lower() + t = 4 if ("san" in gid or "sa" == gid or " san " in gid) else 1 + + mesh_count = ide_exporter.get_prop(obj, "ide_meshes", int, None) + if t in (1, 2, 3): + if mesh_count is None or mesh_count <= 0: + mesh_count = t + + # from GTAMods + if t == 4: + d1 = float(draws[0]) + return f"{model_id}, {model_name}, {txd_name}, {d1:.0f}, {flags}" + + if t == 1: + d1 = float(draws[0]) + return f"{model_id}, {model_name}, {txd_name}, {mesh_count}, {d1:.0f}, {flags}" + + if t == 2: + d1 = float(draws[0]); d2 = float(draws[1] if len(draws) > 1 else d1) + return f"{model_id}, {model_name}, {txd_name}, {mesh_count}, {d1:.0f}, {d2:.0f}, {flags}" + + # t == 3 + d1 = float(draws[0]) + d2 = float(draws[1] if len(draws) > 1 else d1) + d3 = float(draws[2] if len(draws) > 2 else d1) + return f"{model_id}, {model_name}, {txd_name}, {mesh_count}, {d1:.0f}, {d2:.0f}, {d3:.0f}, {flags}" + ####################################################### + @staticmethod + def fmt_tobj(obj): + base = ide_exporter.fmt_objs(obj) + if not base: + return None + on_ = ide_exporter.get_prop(obj, "ide_time_on", int) + off_ = ide_exporter.get_prop(obj, "ide_time_off", int) + if on_ is None or off_ is None: + return None + return f"{base}, {on_}, {off_}" + ####################################################### + @staticmethod + def fmt_anim(obj): + base = ide_exporter.fmt_objs(obj) + if not base: + return None + anim = ide_exporter.get_prop(obj, "ide_anim", str) + if not anim: + return None + return f"{base}, {anim}" + + ####################################################### + @staticmethod + def write_section(fh, name, objects, formatter): + lines = [] + for o in objects: + s = formatter(o) + if s: + lines.append(s) + if lines: + SectionUtility(name).write(fh, lines) + ####################################################### + @staticmethod + def export_ide(filename, context=None): + self = ide_exporter + context = context or bpy.context + self.collect_objs(context) + + folder = os.path.dirname(filename) + if folder and not os.path.exists(folder): + os.makedirs(folder, exist_ok=True) + + with open(filename, "w", encoding="ascii", errors="replace", newline="\n") as fh: + fh.write("# IDE generated with DragonFF\n\n") + self.write_section(fh, "objs", self.objs, self.fmt_objs) + self.write_section(fh, "tobj", self.tobj, self.fmt_tobj) + self.write_section(fh, "anim", self.anim, self.fmt_anim) +####################################################### +def export_ide(options): + """IDE export functions""" + ide_exporter.only_selected = bool(options.get('only_selected', False)) + ide_exporter.game_id = options.get('game_id', None) + ide_exporter.export_objs = bool(options.get('export_objs', True)) + ide_exporter.export_tobj = bool(options.get('export_tobj', False)) + ide_exporter.export_anim = bool(options.get('export_anim', False)) + ide_exporter.export_ide(options['file_name']) +####################################################### +class pwn_exporter: + """Export a Pawn script""" + only_selected = False + game_id = None + stream_distance = 300.0 + draw_distance = 300.0 + x_offset = 0.0 + y_offset = 0.0 + z_offset = 0.0 + + write_artconfig = True + model_directory = "" + base_id = 19379 + id_start = -1000 + id_min = -40000 + + inst_reps = [] + total_objects_num = 0 + + ####################################################### + @staticmethod + def _skip_lod(obj, context): + dff = getattr(context.scene, "dff", None) + if not dff: + return False + if not getattr(dff, "skip_lod", False): + return False + n = obj.name + return n.startswith("LOD") or ".ColMesh" in n + + ####################################################### + @staticmethod + def _is_obj_mesh(obj): + if obj.type != "MESH": + return False + if ".ColMesh" in obj.name: + return False + dff_tag = getattr(obj, "dff", None) + return bool(dff_tag and getattr(dff_tag, "type", None) == "OBJ") + + ####################################################### + @staticmethod + def _root_anchor(obj): + a = obj + while a.parent and a.parent.type == "EMPTY": + a = a.parent + return a + + ####################################################### + @staticmethod + def collect_instances(context): + self = pwn_exporter + self.inst_reps = [] + seen = set() + for obj in context.scene.objects: + if self.only_selected and not obj.select_get(): + continue + if not self._is_obj_mesh(obj): + continue + if self._skip_lod(obj, context): + continue + root = self._root_anchor(obj) + key = root.as_pointer() + if key in seen: + continue + seen.add(key) + self.inst_reps.append(obj) + self.total_objects_num = len(self.inst_reps) + + ####################################################### + @staticmethod + def _inst_transform(obj): + parent = obj.parent + if parent and parent.type == 'EMPTY': + loc = parent.location + rx, ry, rz = euler_xyz(parent) + else: + loc = obj.location + rx, ry, rz = euler_xyz(obj) + x = loc.x + pwn_exporter.x_offset + y = loc.y + pwn_exporter.y_offset + z = loc.z + pwn_exporter.z_offset + return x, y, z, (rx, ry, rz) + + ####################################################### + @staticmethod + def _model_meta(obj): + dm = getattr(obj, "dff_map", None) + + model_id = int(getattr(dm, "object_id", 0)) if dm else 0 + interior = int(getattr(dm, "interior", -1)) if dm else -1 + + base_name = obj.name.split('.')[0] + + modelname = (getattr(dm, "model_name", None) or base_name) if dm else base_name + + dff_name = ( + (getattr(dm, "pawn_model_name", None) or getattr(dm, "ide_model_name", None)) if dm else None + ) or obj.get('DFF_Name', base_name) + + txd_name = ( + (getattr(dm, "pawn_txd_name", None) or getattr(dm, "ide_txd_name", None)) if dm else None + ) or obj.get('TXD_Name', base_name) + + return model_id, interior, modelname, dff_name, txd_name + ####################################################### + @staticmethod + def export_pawn(filename): + self = pwn_exporter + ctx = bpy.context + self.collect_instances(ctx) + if not self.total_objects_num: + return + + folder = os.path.dirname(filename) + if folder and not os.path.exists(folder): + os.makedirs(folder, exist_ok=True) + artconfig_path = os.path.join(folder, "artconfig.txt") + + next_id = self.id_start + name_to_id = {} + + with open(filename, "w", encoding="ascii", errors="replace", newline="\n") as fpwn, \ + open(artconfig_path, "w", encoding="ascii", errors="replace", newline="\n") as facfg: + + fpwn.write("// Pawn generated with DragonFF\n") + fpwn.write("// Objects: %d\n\n" % self.total_objects_num) + if self.write_artconfig: + facfg.write("// artconfig generated with DragonFF\n") + + for obj in self.inst_reps: + base_name = obj.name.split('.')[0] + if base_name not in name_to_id: + model_new_id = next_id + next_id -= 1 + if next_id <= self.id_min: + next_id -= 1000 + name_to_id[base_name] = model_new_id + else: + model_new_id = name_to_id[base_name] + + x, y, z, (rx, ry, rz) = self._inst_transform(obj) + _orig, interior, modelname, dff_name, txd_name = self._model_meta(obj) + + fpwn.write( + f"CreateDynamicObject({model_new_id}, {x:.3f}, {y:.3f}, {z:.3f}, " + f"{rx:.3f}, {ry:.3f}, {rz:.3f}, -1, {interior}, -1, " + f"{self.stream_distance:.3f}, {self.draw_distance:.3f}); // {modelname}\n" + ) + + if 'LODIndex' in obj: + lod_index = int(obj['LODIndex']) + fpwn.write( + f"CreateDynamicObject({lod_index}, {x:.3f}, {y:.3f}, {z:.3f}, " + f"{rx:.3f}, {ry:.3f}, {rz:.3f}, -1, {interior}, -1, " + f"{self.stream_distance:.3f}, {self.draw_distance:.3f}); // LOD for {modelname}\n" + ) + + if self.write_artconfig: + prefix = self.model_directory.strip().replace("\\", "/") + if prefix and not prefix.endswith("/"): + prefix += "/" + facfg.write( + f"AddSimpleModel(-1, {self.base_id}, {model_new_id}, " + f"\"{prefix}{dff_name}.dff\", \"{prefix}{txd_name}.txd\"); // {modelname}\n" + ) + +####################################################### +def export_pawn(options): + """Pawn export functions""" + pwn_exporter.only_selected = bool(options.get('only_selected', False)) + pwn_exporter.game_id = options.get('game_id', None) + pwn_exporter.stream_distance = float(options.get('stream_distance', 300.0)) + pwn_exporter.draw_distance = float(options.get('draw_distance', 300.0)) + pwn_exporter.x_offset = float(options.get('x_offset', 0.0)) + pwn_exporter.y_offset = float(options.get('y_offset', 0.0)) + pwn_exporter.z_offset = float(options.get('z_offset', 0.0)) + + pwn_exporter.write_artconfig = bool(options.get('write_artconfig', True)) + pwn_exporter.model_directory = str(options.get('model_directory', "") or "") + pwn_exporter.base_id = int(options.get('base_id', 19379)) + pwn_exporter.id_start = int(options.get('id_start', -1000)) + pwn_exporter.id_min = int(options.get('id_min', -40000)) + + pwn_exporter.export_pawn(options['file_name']) \ No newline at end of file diff --git a/ops/map_importer.py b/ops/map_importer.py index 6e98c9d..0f199d7 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 .cull_importer import cull_importer @@ -29,13 +30,58 @@ class map_importer: object_instances = [] cull_instances = [] col_files = [] + garage_instances = [] + garage_collection_name = None + enex_instances = [] collision_collection = None object_instances_collection = None mesh_collection = None cull_collection = None + enex_collection = None + enex_collection_name = None map_section = "" settings = None + ####################################################### + @staticmethod + def fix_id(idblock): + if idblock is None: + return False + try: + _ = idblock.name + return True + except ReferenceError: + return False + ####################################################### + @staticmethod + def create_grge_collection(context): + self = map_importer + if self.settings is None: + self.settings = context.scene.dff + + grge_name = f"{self.settings.game_version_dropdown} GRGE" + + coll = self.garage_collection if map_importer.fix_id(self.garage_collection) else None + if coll is None and self.garage_collection_name: + coll = bpy.data.collections.get(self.garage_collection_name) + + if coll is None: + coll = bpy.data.collections.get(grge_name) + if coll is None: + coll = bpy.data.collections.new(grge_name) + + if coll.name not in {c.name for c in context.scene.collection.children}: + context.scene.collection.children.link(coll) + + self.garage_collection = coll + self.garage_collection_name = coll.name + return coll + ####################################################### + 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): @@ -65,6 +111,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 @@ -91,6 +147,8 @@ def import_object_instance(context, inst): obj, inst ) + + cached_2dfx = [obj for obj in model_cache if obj.dff.type == "2DFX"] for obj in cached_2dfx: new_obj = bpy.data.objects.new(obj.name, obj.data) @@ -101,6 +159,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] @@ -167,6 +227,17 @@ def import_object_instance(context, inst): obj, inst ) + # Assign IPL Data + for obj in root_objects: + if getattr(getattr(obj, "dff", None), "type", None) == "OBJ": + self.assign_map_properties(obj, { + "object_id": int(inst.id), + "model_name": str(model), + "interior": int(getattr(inst, "interior", 0)), + "lod": int(getattr(inst, "lod", -1)), + }) + obj["LODIndex"] = int(getattr(inst, "lod", -1)) + # Set root object as 2DFX parent if root_objects: for obj in collection_objects: @@ -234,6 +305,187 @@ def import_cull(context, cull): ####################################################### @staticmethod + def create_grge_sphere(): + name = "_GRGE_" + me = bpy.data.meshes.get(name) + if me: + return me + + import bmesh + me = bpy.data.meshes.new(name) + bm = bmesh.new() + bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=1.25) + bm.to_mesh(me) + bm.free() + return me + + ####################################################### + @staticmethod + def import_garage(context, g): + self = map_importer + if not self.garage_collection: + self.create_garage_collection(context) + + # calculate the GRGE center + x0, y0, z0 = float(g.posX), float(g.posY), float(g.posZ) + lx, ly = float(g.lineX), float(g.lineY) + x1, y1, z1 = float(g.cubeX), float(g.cubeY), float(g.cubeZ) + cx = (x0 + (x0+lx) + x1 + (x1-lx)) * 0.25 + cy = (y0 + (y0+ly) + y1 + (y1-ly)) * 0.25 + cz = (z0 + z1) * 0.5 + + # create a tiny blue sphere mesh + me = self.create_grge_sphere() + name = f"GRGE_{getattr(g,'name','') or 'Garage'}" + sphere = bpy.data.objects.new(name, me) + sphere.location = (cx, cy, cz) + sphere.hide_render = True + + mat = bpy.data.materials.get("_GRGE") or bpy.data.materials.new("_GRGE") + mat.diffuse_color = (0.0, 0.35, 1.0, 1.0) + if not sphere.data.materials: + sphere.data.materials.append(mat) + else: + sphere.data.materials[0] = mat + + sphere["grge_flag"] = int(getattr(g, 'doorType', "")) + sphere["grge_type"] = int(getattr(g, 'garageType', "")) + sphere["grge_name"] = str(getattr(g, 'name', "")) + + for k in ("posX","posY","posZ","lineX","lineY","cubeX","cubeY","cubeZ","rotZ"): + if hasattr(g, k): + sphere[f"grge_{k}"] = float(getattr(g, k)) + + coll = map_importer.create_grge_collection(context) + coll.objects.link(sphere) + + sphere.dff.type = "GRGE" + + if hasattr(sphere, "dff_map"): + sphere.dff_map.ipl_section = "grge" + else: + sphere["ipl_section"] = "grge" + + ####################################################### + @staticmethod + def create_enex_cylinder(): + name = "_ENEX_" + me = bpy.data.meshes.get(name) + if me: + return me + + import bmesh + me = bpy.data.meshes.new(name) + bm = bmesh.new() + bmesh.ops.create_cone( + bm, + segments = 24, + radius1 = 0.45, + radius2 = 0.45, + depth = 1.0, + cap_ends = True, + cap_tris = False + ) + bm.to_mesh(me) + bm.free() + return me + ####################################################### + @staticmethod + def import_enex(context, e): + self = map_importer + + if isinstance(e, (list, tuple)): + row = list(e) + if len(row) < 18: + row += [None] * (18 - len(row)) + + X1, Y1, Z1 = row[0], row[1], row[2] + EnterAngle = row[3] + SizeX, SizeY, SizeZ = row[4], row[5], row[6] + X2, Y2, Z2 = row[7], row[8], row[9] + ExitAngle = row[10] + TargetInterior = row[11] + Flags = row[12] + raw_name = row[13] + name = None + if isinstance(raw_name, str): + s = raw_name.strip() + if len(s) >= 2 and ((s[0] == '"' and s[-1] == '"') or (s[0] == "'" and s[-1] == "'")): + s = s[1:-1] + name = s + Sky = row[14] + NumPedsToSpawn = row[15] + TimeOn = row[16] + TimeOff = row[17] + + coll = self.create_enex_collection(context) + me = self.create_enex_cylinder() + + label = f"ENEX_{name}" if name else "ENEX" + obj = bpy.data.objects.new(label, me) + + try: + obj.location = (float(X1 or 0.0), float(Y1 or 0.0), float(Z1 or 0.0)) + except Exception: + obj.location = (0.0, 0.0, 0.0) + + obj.rotation_mode = 'ZXY' + try: + obj.rotation_euler = (0.0, 0.0, float(EnterAngle or 0.0)) + except Exception: + obj.rotation_euler = (0.0, 0.0, 0.0) + + obj.hide_render = True + try: + obj.display_type = 'WIRE' + except Exception: + pass + + mat = bpy.data.materials.get("_ENEX") or bpy.data.materials.new("_ENEX") + mat.diffuse_color = (1.0, 0.85, 0.10, 1.0) + if not obj.data.materials: + obj.data.materials.append(mat) + else: + obj.data.materials[0] = mat + + # tag section + if hasattr(obj, "dff"): + obj.dff.type = "ENEX" + if hasattr(obj, "dff_map"): + obj.dff_map.ipl_section = "enex" + else: + obj["ipl_section"] = "enex" + + # store all fields so you can round-trip/export later + def _setf(k, v): + try: + if v is not None: obj[f"enex_{k}"] = float(v) + except Exception: + pass + def _seti(k, v): + try: + if v is not None: obj[f"enex_{k}"] = int(v) + except Exception: + pass + def _sets(k, v): + if v is not None: obj[f"enex_{k}"] = str(v) + + _setf("X1", X1); _setf("Y1", Y1); _setf("Z1", Z1) + _setf("EnterAngle", EnterAngle) + _setf("SizeX", SizeX); _setf("SizeY", SizeY); _setf("SizeZ", SizeZ) + _setf("X2", X2); _setf("Y2", Y2); _setf("Z2", Z2) + _setf("ExitAngle", ExitAngle) + _seti("TargetInterior", TargetInterior) + _seti("Flags", Flags) + _sets("Name", name) + _seti("Sky", Sky) + _seti("NumPedsToSpawn", NumPedsToSpawn) + _seti("TimeOn", TimeOn) + _seti("TimeOff", TimeOff) + + coll.objects.link(obj) + ####################################################### + @staticmethod def create_object_instances_collection(context): self = map_importer @@ -285,6 +537,50 @@ def create_cull_collection(context): ####################################################### @staticmethod + def create_garage_collection(context): + self = map_importer + coll_name = '%s GRGE' % self.settings.game_version_dropdown + self.garage_collection = bpy.data.collections.get(coll_name) + if not self.garage_collection: + self.garage_collection = bpy.data.collections.new(coll_name) + context.scene.collection.children.link(self.garage_collection) + # hide by default + context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[coll_name] + context.view_layer.active_layer_collection.hide_viewport = True + + ####################################################### + @staticmethod + def create_enex_collection(context): + self = map_importer + if self.settings is None: + self.settings = context.scene.dff + + coll_name = f"{self.settings.game_version_dropdown} ENEX" + + coll = self.enex_collection if map_importer.fix_id(self.enex_collection) else None + if coll is None and self.enex_collection_name: + coll = bpy.data.collections.get(self.enex_collection_name) + + if coll is None: + coll = bpy.data.collections.get(coll_name) + if coll is None: + coll = bpy.data.collections.new(coll_name) + + if coll.name not in {c.name for c in context.scene.collection.children}: + context.scene.collection.children.link(coll) + + # Optional (nice): start hidden + try: + context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[coll_name] + context.view_layer.active_layer_collection.hide_viewport = True + except Exception: + pass + + self.enex_collection = coll + self.enex_collection_name = coll.name + return coll + ####################################################### + @staticmethod def load_map(settings): self = map_importer @@ -294,6 +590,8 @@ def load_map(settings): self.mesh_collection = None self.collision_collection = None self.cull_collection = None + self.garage_collection = None + self.enex_instances = [] self.settings = settings if self.settings.use_custom_map_section: @@ -316,6 +614,16 @@ def load_map(settings): else: self.cull_instances = [] + if self.settings.load_grge: + self.garage_instances = map_data.garage_instances + else: + self.garage_instances = [] + + if self.settings.load_enex: + self.enex_instances = map_data.enex_instances + else: + self.enex_instances = [] + if self.settings.load_collisions: # Get a list of the .col files available From e385f2ffac5abb641d380fb8d3b0ea81164d2f22 Mon Sep 17 00:00:00 2001 From: Chris <120978532+spicybung@users.noreply.github.com> Date: Sun, 24 Aug 2025 01:41:41 -0400 Subject: [PATCH 2/6] Fix typo Fixes typo "objcets" to "object" in map_ot that was hanging around before i changed anything --- map_ot.py | 908 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 908 insertions(+) create mode 100644 map_ot.py diff --git a/map_ot.py b/map_ot.py new file mode 100644 index 0000000..1b504ab --- /dev/null +++ b/map_ot.py @@ -0,0 +1,908 @@ +# GTA DragonFF - Blender scripts to edit basic GTA formats +# Copyright (C) 2019 Parik + +# 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 3 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, see . + +import bpy +import math +import os +import time +import bmesh + +from bpy.props import StringProperty, CollectionProperty +from bpy_extras.io_utils import ImportHelper, ExportHelper + + +from ..ops import map_exporter, map_importer +from ..ops.cull_importer import cull_importer +from ..ops.importer_common import link_object + + +####################################################### +class SCENE_OT_dff_import_map(bpy.types.Operator): + """Tooltip""" + bl_idname = "scene.dragonff_map_import" + bl_label = "Import map section" + + _timer = None + _updating = False + _progress_current = 0 + _progress_total = 0 + _importer = None + + _inst_index = 0 + _inst_loaded = False + + _col_index = 0 + _col_loaded = True + + _cull_loaded = True + + _grge_loaded = True + _enex_loaded = True + + ####################################################### + def modal(self, context, event): + + if event.type in {'ESC'}: + self.cancel(context) + return {'CANCELLED'} + + if event.type == 'TIMER' and not self._updating: + self._updating = True + + importer = self._importer + + # Import CULL if there are any left to load + if not self._cull_loaded: + + for cull in importer.cull_instances: + importer.import_cull(context, cull) + + self._progress_current += 1 + self._cull_loaded = True + + # Import Garages if there are any left to load + elif not self._grge_loaded: + for g in getattr(importer, 'garage_instances', []): + try: + importer.import_garage(context, g) + except Exception as ex: + print("Can't import GRGE... skipping", ex) + self._progress_current += 1 + self._grge_loaded = True + + # Import Enex checkpoints if there are any left to load + elif not self._enex_loaded: + for e in self._importer.enex_instances: + try: + self._importer.import_enex(context, e) + except Exception as ex: + print("Can't import ENEX... skipping", ex) + self._progress_current += 1 + self._enex_loaded = True + + # Import collision files if there are any left to load + elif not self._col_loaded: + num_objects_at_once = 5 + cols_num = len(importer.col_files) + + for _ in range(num_objects_at_once): + if self._col_index >= cols_num: + self._col_loaded = True + break + + # Fetch next collision + col_file = importer.col_files[self._col_index] + self._col_index += 1 + + importer.import_collision(context, col_file) + self._progress_current += 1 + + # Import object instances + else: + # As the number of objects increases, loading performance starts to get crushed by scene updates, so + # we try to keep loading at least 5% of the total scene object count on each timer pulse. + num_objects_at_once = max(10, int(0.05 * len(bpy.data.objects))) + instances_num = len(importer.object_instances) + + for _ in range(num_objects_at_once): + if self._inst_index >= instances_num: + self._inst_loaded = True + break + + # Fetch next instance + inst = importer.object_instances[self._inst_index] + self._inst_index += 1 + + try: + importer.import_object_instance(context, inst) + except: + print("Can`t import model... skipping") + + self._progress_current += 1 + + # Update cursor progress indicator if something needs to be loaded + progress = ( + float(self._progress_current) / float(self._progress_total) + ) if self._progress_total else 100 + + context.window_manager.progress_update(progress) + + # Update dependency graph + dg = context.evaluated_depsgraph_get() + dg.update() + + self._updating = False + + if self._inst_loaded: + self.cancel(context) + return {'FINISHED'} + + return {'PASS_THROUGH'} + + ####################################################### + def execute(self, context): + + settings = context.scene.dff + self._importer = map_importer.load_map(settings) + + self._progress_current = 0 + self._progress_total = 0 + + self._inst_index = 0 + self._inst_loaded = False + self._progress_total += len(self._importer.object_instances) + + if self._importer.cull_instances: + self._cull_loaded = False + self._progress_total += 1 + else: + self._cull_loaded = True + + if self._importer.garage_instances: + self._grge_loaded = False + self._progress_total += 1 + else: + self._grge_loaded = True + + if self._importer.col_files: + self._col_index = 0 + self._col_loaded = False + self._progress_total += len(self._importer.col_files) + else: + self._col_loaded = True + + if self._importer.enex_instances: + self._enex_loaded = False + self._progress_total += 1 + else: + self._enex_loaded = True + + wm = context.window_manager + wm.progress_begin(0, 100.0) + + # Call the "modal" function every 0.1s + self._timer = wm.event_timer_add(0.1, window=context.window) + wm.modal_handler_add(self) + + return {'RUNNING_MODAL'} + + ####################################################### + def cancel(self, context): + wm = context.window_manager + wm.progress_end() + wm.event_timer_remove(self._timer) + +####################################################### +class SCENE_OT_ipl_select(bpy.types.Operator, ImportHelper): + + bl_idname = "scene.select_ipl" + bl_label = "Select IPL File" + + filename_ext = ".ipl" + + filter_glob : bpy.props.StringProperty( + default="*.ipl", + options={'HIDDEN'}) + + def invoke(self, context, event): + self.filepath = context.scene.dff.game_root + "/data/maps/" + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + def execute(self, context): + if os.path.splitext(self.filepath)[-1].lower() == self.filename_ext: + filepath = os.path.normpath(self.filepath) + # Try to find if the file is within the game directory structure + sep_pos = filepath.upper().find(f"data{os.sep}maps") + + if sep_pos != -1: + # File is within game directory, use relative path + game_root = filepath[:sep_pos] + context.scene.dff.game_root = game_root + context.scene.dff.custom_ipl_path = os.path.relpath(filepath, game_root) + else: + # File is outside game directory, use absolute path + # Don't change game_root, keep the existing one + context.scene.dff.custom_ipl_path = filepath + return {'FINISHED'} + +####################################################### +class EXPORT_OT_ipl(bpy.types.Operator, ExportHelper): + bl_idname = "export_scene.dff_ipl" + bl_label = "DragonFF IPL Export" + bl_description = "Export a GTA IPL file with INST, CULL, or both sections" + filename_ext = ".ipl" + + export_inst: bpy.props.BoolProperty( + name="Export INST (object placements)", + default=True, + description="Export object placement (INST) section" + ) + export_cull: bpy.props.BoolProperty( + name="Export CULL zones", + default=True, + description="Export CULL (zone) section" + ) + + export_grge: bpy.props.BoolProperty( + name="Export GRGE zones", + default=True, + description="Export GRGE (zone) section" + ) + + export_enex: bpy.props.BoolProperty( + name="Export ENEX zones", + default=True, + description="Export ENEX (zone) section" + ) + + only_selected: bpy.props.BoolProperty( + name="Only Selected", + default=False + ) + 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" + ) + + 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="*.ipl", + options={'HIDDEN'} + ) + + ####################################################### + def draw(self, context): + layout = self.layout + layout.prop(self, "export_inst") + layout.prop(self, "export_cull") + layout.prop(self, "export_grge") + layout.prop(self, "export_enex") + layout.prop(self, "only_selected") + 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): + start = time.time() + try: + export_inst = self.export_inst + export_cull = self.export_cull + export_grge = self.export_grge + export_enex = self.export_enex + map_exporter.export_ipl( + { + "file_name": self.filepath, + "only_selected": self.only_selected, + "game_id": context.scene.dff.game_version_dropdown, + "export_inst": export_inst, + "export_cull": export_cull, + "export_grge": export_grge, + "export_enex": export_enex, + "x_offset": self.x_offset, + "y_offset": self.y_offset, + "z_offset": self.z_offset, + } + ) + + if not map_exporter.ipl_exporter.total_objects_num: + self.report({"ERROR"}, "No objects with IPL data found") + return {'CANCELLED'} + + self.report({"INFO"}, f"Finished export in {time.time() - start:.2f}s") + + except Exception as e: + self.report({"ERROR"}, str(e)) + return {'CANCELLED'} + + 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): + + bl_idname = "object.dff_add_cull" + bl_label = "Add CULL Zone" + bl_description = "Add CULL zone to the scene" + bl_options = {'REGISTER', 'UNDO'} + + location: bpy.props.FloatVectorProperty( + name="Location", + description="Location for the newly added object", + subtype='XYZ', + default=(0, 0, 0) + ) + + scale: bpy.props.FloatVectorProperty( + name="Scale", + description="Scale for the newly added object", + subtype='XYZ', + default=(1, 1, 1) + ) + + angle: bpy.props.FloatProperty( + name="Angle", + description="Angle along the Z axis", + subtype='ANGLE', + min=-math.pi * 2, + max=math.pi * 2, + step=100, + default=0 + ) + + ####################################################### + def invoke(self, context, event): + self.location = context.scene.cursor.location + return self.execute(context) + + ####################################################### + def execute(self, context): + obj = cull_importer.create_cull_object( + location=self.location, + scale=self.scale, + flags=0, + angle=self.angle + ) + link_object(obj, context.collection) + + context.view_layer.objects.active = obj + for o in context.selected_objects: + o.select_set(False) + obj.select_set(True) + + return {'FINISHED'} +####################################################### +class OBJECT_OT_dff_add_garage(bpy.types.Operator): + bl_idname = "object.dff_add_garage" + bl_label = "Add GRGE Zone" + bl_description = "Add a GRGE zone to the scene" + bl_options = {'REGISTER', 'UNDO'} + + location: bpy.props.FloatVectorProperty( + name="Location", + description="Location for the newly added garage sphere", + subtype='XYZ', + default=(0.0, 0.0, 0.0) + ) + + grge_type: bpy.props.IntProperty( + name="Garage Type", + description="Garage type ID", + default=5 + ) + + grge_flag: bpy.props.IntProperty( + name="Garage Flag", + description="Garage flag value", + default=0 + ) + + grge_name: bpy.props.StringProperty( + name="Garage Name", + description="Optional garage name", + default="Garage" + ) + + ####################################################### + def invoke(self, context, event): + self.location = context.scene.cursor.location + return self.execute(context) + + ####################################################### + def execute(self, context): + MapImporter = map_importer.map_importer + if getattr(MapImporter, "settings", None) is None: + MapImporter.settings = context.scene.dff + + # Always resolve the correct collection dynamically (III/VC/SA/etc.) + coll = MapImporter.create_grge_collection(context) + + me = MapImporter.create_grge_sphere() + obj = bpy.data.objects.new(f"GRGE_{self.grge_name}", me) + obj.location = self.location + obj.hide_render = True + + mat = bpy.data.materials.get("_GRGE") or bpy.data.materials.new("_GRGE") + mat.diffuse_color = (0.0, 0.35, 1.0, 1.0) + if not obj.data.materials: + obj.data.materials.append(mat) + else: + obj.data.materials[0] = mat + + obj.dff.type = "GRGE" + if hasattr(obj, "dff_map"): + obj.dff_map.ipl_section = "grge" + else: + obj["ipl_section"] = "grge" + + obj["grge_type"] = int(self.grge_type) + obj["grge_flag"] = int(self.grge_flag) + obj["grge_name"] = self.grge_name + + for k in ("grge_posX","grge_posY","grge_posZ","grge_lineX","grge_lineY", + "grge_cubeX","grge_cubeY","grge_cubeZ"): + if k not in obj: + obj[k] = 0.0 + + # Link exactly like add_cull does, but to the resolved GRGE collection + link_object(obj, coll) + + context.view_layer.objects.active = obj + for o in context.selected_objects: + o.select_set(False) + obj.select_set(True) + + return {'FINISHED'} +####################################################### +class OBJECT_OT_dff_add_enex(bpy.types.Operator): + bl_idname = "object.dff_add_enex" + bl_label = "Add ENEX Zone" + bl_description = "Add an ENEX (entry/exit) marker to the scene (wireframe cylinder, size 1.24)" + bl_options = {'REGISTER', 'UNDO'} + + location: bpy.props.FloatVectorProperty( + name="Location", + description="Location for the newly added ENEX marker", + subtype='XYZ', + default=(0.0, 0.0, 0.0) + ) + angle: bpy.props.FloatProperty( + name="Angle", + description="Angle around Z (radians)", + subtype='ANGLE', + default=0.0 + ) + name_hint: bpy.props.StringProperty( + name="Name", + description="Optional ENEX name", + default="ENEX" + ) + ####################################################### + def invoke(self, context, event): + self.location = context.scene.cursor.location + return self.execute(context) + ####################################################### + def execute(self, context): + MapImporter = map_importer.map_importer + if getattr(MapImporter, "settings", None) is None: + MapImporter.settings = context.scene.dff + + coll = MapImporter.create_enex_collection(context) + + try: + me = MapImporter.create_enex_cylinder() + except AttributeError: + me = bpy.data.meshes.new("_ENEX_") + bm = bmesh.new() + bmesh.ops.create_cone( + bm, segments=24, + radius1=1.24, radius2=1.24, depth=1.24, + cap_ends=True, cap_tris=False + ) + bm.to_mesh(me); bm.free() + + obj = bpy.data.objects.new( + f"ENEX_{self.name_hint}" if self.name_hint else "ENEX", me + ) + obj.location = self.location + obj.rotation_mode = 'ZXY' + obj.rotation_euler = (0.0, 0.0, float(self.angle)) + + obj.hide_render = True + try: + obj.display_type = 'WIRE' + except Exception: + pass + + mat = bpy.data.materials.get("_ENEX") or bpy.data.materials.new("_ENEX") + mat.diffuse_color = (1.0, 0.85, 0.10, 1.0) + if not obj.data.materials: + obj.data.materials.append(mat) + else: + obj.data.materials[0] = mat + + if hasattr(obj, "dff"): + obj.dff.type = "ENEX" + if hasattr(obj, "dff_map"): + obj.dff_map.ipl_section = "enex" + else: + obj["ipl_section"] = "enex" + + obj["enex_name"] = self.name_hint + obj["enex_posX"] = float(obj.location.x) + obj["enex_posY"] = float(obj.location.y) + obj["enex_posZ"] = float(obj.location.z) + obj["enex_rotZ"] = float(self.angle) + + link_object(obj, coll) + + context.view_layer.objects.active = obj + for o in context.selected_objects: + o.select_set(False) + 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 From adc0e60333ca6e2bebe21f1fd96a91d3b9c2d20c Mon Sep 17 00:00:00 2001 From: Psycrow Date: Mon, 25 Aug 2025 14:19:28 +0300 Subject: [PATCH 3/6] Reverted outdated changes --- __init__.py | 3 +-- gtaLib/txd.py | 7 ++----- ops/dff_exporter.py | 17 ++++------------- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/__init__.py b/__init__.py index e34f10e..743a5c2 100644 --- a/__init__.py +++ b/__init__.py @@ -37,14 +37,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.EXPORT_OT_ide, - gui.EXPORT_OT_pawn, gui.OBJECT_OT_dff_generate_bone_props, gui.OBJECT_OT_dff_set_parent_bone, gui.OBJECT_OT_dff_clear_parent_bone, diff --git a/gtaLib/txd.py b/gtaLib/txd.py index 2e15414..a257292 100644 --- a/gtaLib/txd.py +++ b/gtaLib/txd.py @@ -636,11 +636,8 @@ def from_mem(data): ) = unpack_from(" Date: Tue, 26 Aug 2025 23:21:27 -0400 Subject: [PATCH 4/6] Delete ops/map_exporter.py --- ops/map_exporter.py | 759 -------------------------------------------- 1 file changed, 759 deletions(-) delete mode 100644 ops/map_exporter.py diff --git a/ops/map_exporter.py b/ops/map_exporter.py deleted file mode 100644 index c9383d3..0000000 --- a/ops/map_exporter.py +++ /dev/null @@ -1,759 +0,0 @@ -# GTA DragonFF - Blender scripts to edit basic GTA formats -# Copyright (C) 2019 Parik - -# 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 3 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, see . - -import os -import bpy - -from ..gtaLib.map import TextIPLData, MapDataUtility, SectionUtility -from ..gtaLib.data import map_data -from .cull_exporter import cull_exporter - - -####################################################### -def quat_xyzw(obj_or_empty): - """Quaternion (x,y,z,w).""" - q = getattr(obj_or_empty, "rotation_quaternion", None) - if q is None: - e = getattr(obj_or_empty, "rotation_euler", None) - q = (e.to_quaternion() if e is not None else None) - if q is None: - return 0.0, 0.0, 0.0, 1.0 - q = q.normalized() - return float(q.x), float(q.y), float(q.z), float(q.w) - -####################################################### -def euler_xyz(obj_or_empty): - """Euler XYZ in degrees.""" - e = getattr(obj_or_empty, "rotation_euler", None) - if e is None: - q = getattr(obj_or_empty, "rotation_quaternion", None) - e = (q.to_euler('XYZ') if q is not None else None) - if e is None: - return 0.0, 0.0, 0.0 - rad2deg = 180.0 / 3.141592653589793 - return float(e.x*rad2deg), float(e.y*rad2deg), float(e.z*rad2deg) -####################################################### -class ipl_exporter: - only_selected = False - game_id = None - export_inst = False - export_cull = False - export_grge = False - export_enex = False - - x_offset = 0.0 - y_offset = 0.0 - z_offset = 0.0 - - inst_objects = [] - cull_objects = [] - grge_objects = [] - enex_objects = [] - total_objects_num = 0 - ####################################################### - def _q(obj, key, default): - try: - return obj.get(key, default) - except Exception: - return default - ####################################################### - @staticmethod - def _anchor_and_pos(obj): - """Pick the transform carrier.""" - carrier = obj.parent if (obj.parent and obj.parent.type == 'EMPTY') else obj - loc = carrier.location - x = float(loc.x) + ipl_exporter.x_offset - y = float(loc.y) + ipl_exporter.y_offset - z = float(loc.z) + ipl_exporter.z_offset - return carrier, x, y, z - ####################################################### - @staticmethod - def _resolve_meta(obj): - """Read id/name/interior/lod.""" - dm = getattr(obj, "dff_map", None) - - # ID - object_id = ( - int(getattr(dm, "object_id", 0)) if dm else - int(obj.get("object_id", 0)) - ) - - # Model name - base_name = obj.name.split('.')[0] - model_name = ( - getattr(dm, "model_name", None) or - obj.get("model_name", None) or - base_name - ) - - # Interior - interior = ( - int(getattr(dm, "interior", 0)) if dm else - int(obj.get("interior", 0)) - ) - - # LOD index - if "LODIndex" in obj: - lod_index = int(obj["LODIndex"]) - else: - lod_index = int(getattr(dm, "lod", -1)) if dm else int(obj.get("lod", -1)) - - return object_id, model_name, interior, lod_index - ####################################################### - @staticmethod - def collect_objects(context): - self = ipl_exporter - self.inst_objects = [] - self.cull_objects = [] - self.grge_objects = [] - self.enex_objects = [] - - for obj in context.scene.objects: - if self.only_selected and not obj.select_get(): - continue - - dff_tag = getattr(obj, "dff", None) - dff_type = getattr(dff_tag, "type", None) - - # INST entries - if self.export_inst and obj.type == 'MESH' and dff_type == 'OBJ': - if self._skip_lod_or_col(obj, context): - continue - self.inst_objects.append(obj) - continue - - # CULL entries - if self.export_cull and obj.type == 'EMPTY' and dff_type == 'CULL': - self.cull_objects.append(obj) - continue - - # GRGE entries - if self.export_grge and obj.type == 'MESH' and dff_type == 'GRGE': - self.grge_objects.append(obj) - continue - - # ENEX entries - if self.export_enex and obj.type in {'MESH','EMPTY'} and dff_type == 'ENEX': - self.enex_objects.append(obj) - continue - - self.total_objects_num = len(self.inst_objects) + len(self.cull_objects) + len(self.grge_objects) + len(self.enex_objects) - - ####################################################### - @staticmethod - def _skip_lod_or_col(obj, context): - dff_scene = getattr(context.scene, "dff", None) - if dff_scene and getattr(dff_scene, "skip_lod", False): - if obj.name.startswith("LOD"): - return True - if ".ColMesh" in obj.name: - return True - return False - - ####################################################### - @staticmethod - def format_inst_line(obj, context=None): - ####################################################### - def round_float(v: float, places=10) -> str: - s = f"{v:.{places}f}".rstrip("0").rstrip(".") - return s if s else "0" - ####################################################### - def round_quat(v: float) -> str: - av = abs(v) - if av != 0.0 and av < 1e-7: - return f"{v:.10e}".replace("e-0", "e-").replace("e+0", "e+") - places = 9 if av >= 0.9 else 11 - return round_float(v, places) - ####################################################### - def format_inst_fields(v, places): - s = f"{v:.{places}f}" - if "." in s: - s = s.rstrip("0").rstrip(".") - if s in ("-0", "-0.0", "-0."): - s = "0" - return s - ####################################################### - if context is not None and ipl_exporter._skip_lod_or_col(obj, context): - return None - - carrier, x, y, z = ipl_exporter._anchor_and_pos(obj) - object_id, model_name, interior, lod_index = ipl_exporter._resolve_meta(obj) - - gid = ipl_exporter.game_id - - # fields - f6 = lambda v: format_inst_fields(v, 7) - f7 = lambda v: format_inst_fields(v, 7) - f10 = lambda v: format_inst_fields(v, 11) - f11 = lambda v: format_inst_fields(v, 11) - - if gid == map_data.game_version.SA: - qx, qy, qz, qw = quat_xyzw(carrier) - - qw_out = -qw - - return ( - f"{object_id}, {model_name}, {interior}, " - f"{round_float(x,6)}, {round_float(y,6)}, {round_float(z,6)}, " - f"{round_quat(qx)}, {round_quat(qy)}, " - f"{round_quat(qz)}, {round_quat(qw_out)}, " - f"{lod_index} # {obj.name}" - ) - - elif gid == map_data.game_version.VC: - sx, sy, sz = getattr(carrier, "scale", (1.0, 1.0, 1.0)) - qx, qy, qz, qw = quat_xyzw(carrier) - if qw < 0.0: - qx, qy, qz, qw = -qx, -qy, -qz, -qw - - return ( - f"{object_id}, {model_name}, {interior}, " - f"{f7(x)}, {f7(y)}, {f7(z)}, " - f"{f6(sx)}, {f6(sy)}, {f6(sz)}, " - f"{f11(qx)}, {f11(qy)}, {f11(qz)}, {f11(qw)}" - ) - - elif gid == map_data.game_version.III: - sx, sy, sz = getattr(carrier, "scale", (1.0, 1.0, 1.0)) - qx, qy, qz, qw = quat_xyzw(carrier) - if qw < 0.0: - qx, qy, qz, qw = -qx, -qy, -qz, -qw - - return ( - f"{object_id}, {model_name}, {interior}, " - f"{f7(x)}, {f7(y)}, {f7(z)}, " - f"{f6(sx)}, {f6(sy)}, {f6(sz)}, " - f"{f10(qx)}, {f10(qy)}, {f10(qz)}, {f10(qw)}" - ) - - else: - ex, ey, ez = euler_xyz(carrier) - return ( - f"{object_id}, {model_name}, " - f"{f6(x)}, {f6(y)}, {f6(z)}, " - f"{f6(ex)}, {f6(ey)}, {f6(ez)}, " - f"{lod_index} # {obj.name}" - ) - ####################################################### - @staticmethod - def format_grge_line(obj): - - # based on GTAMods - px = float(obj.get("grge_posX", obj.location.x)) - py = float(obj.get("grge_posY", obj.location.y)) - pz = float(obj.get("grge_posZ", obj.location.z)) - - lx = float(obj.get("grge_lineX", 0.0)) - ly = float(obj.get("grge_lineY", 0.0)) - - cx = float(obj.get("grge_cubeX", 0.0)) - cy = float(obj.get("grge_cubeY", 0.0)) - cz = float(obj.get("grge_cubeZ", 0.0)) - - gtype = int(obj.get("grge_type", 5)) - flag = int(obj.get("grge_flag", 0)) - - name = str(obj.get("grge_name", obj.name)) - - return (f"{px:.5f}, {py:.5f}, {pz:.5f}, " - f"{lx:.5f}, {ly:.5f}, " - f"{cx:.5f}, {cy:.5f}, {cz:.5f}, " - f"{flag}, {gtype}, {name}") - - ####################################################### - @staticmethod - def format_enex_line(obj): - ex = float(obj.get("enex_X1", obj.location.x)) - ey = float(obj.get("enex_Y1", obj.location.y)) - ez = float(obj.get("enex_Z1", obj.location.z)) - - p0 = float(obj.get("enex_EnterAngle", 0.0)) - - p1 = float(obj.get("enex_SizeX", 2.0)) - p2 = float(obj.get("enex_SizeY", 2.0)) - - p3 = int(obj.get("enex_SizeZ", obj.get("enex_Flags", 8))) - - tx = float(obj.get("enex_X2", ex)) - ty = float(obj.get("enex_Y2", ey)) - tz = float(obj.get("enex_Z2", ez)) - - ang = float(obj.get("enex_ExitAngle", 0.0)) - - interior = int(obj.get("enex_TargetInterior", obj.get("enex_interior", 0))) - mode = int(obj.get("enex_mode", 4)) - - name = obj.get("enex_Name", obj.name) - if name.startswith("ENEX_"): - name = name[5:] - name = f"\"{name}\"" - - t0 = int(obj.get("enex_Sky", 0)) - t1 = int(obj.get("enex_NumPedsToSpawn", 2)) - t2 = int(obj.get("enex_TimeOn", 0)) - t3 = int(obj.get("enex_TimeOff", 24)) - - def f5(v): return f"{v:.5f}" - def g(v): return f"{v:.10g}" - - return ( - f"{f5(ex)}, {f5(ey)}, {f5(ez)}, " - f"{g(p0)}, {g(p1)}, {g(p2)}, {p3}, " - f"{f5(tx)}, {f5(ty)}, {f5(tz)}, " - f"{g(ang)}, {interior}, {mode}, {name}, {t0}, {t1}, {t2}, {t3}" - ) - ####################################################### - def export_ipl(filename): - self = ipl_exporter - self.collect_objects(bpy.context) - if not self.total_objects_num and not (self.export_grge and getattr(self, "grge_objects", None)): - return - - # INST - object_instances = [] - for obj in self.inst_objects: - line = self.format_inst_line(obj, bpy.context) - if line: - object_instances.append(line) - - # CULL - cull_instances = cull_exporter.export_objects(self.cull_objects, self.game_id) - - # GRGE - garage_instances = [] - if self.export_grge and self.grge_objects: - for o in self.grge_objects: - s = self.format_grge_line(o) - if s: - garage_instances.append(s) - - # ENEX - enex_instances = [] - if self.export_enex and self.enex_objects: - for o in self.enex_objects: - s = self.format_enex_line(o) - if s: - enex_instances.append(s) - - # initialize - ipl_data = TextIPLData(object_instances, cull_instances, garage_instances, enex_instances) - MapDataUtility.write_ipl_data(filename, self.game_id, ipl_data) - -####################################################### -def export_ipl(options): - ipl_exporter.only_selected = bool(options.get('only_selected', False)) - ipl_exporter.game_id = options.get('game_id', None) - ipl_exporter.export_inst = bool(options.get('export_inst', True)) - ipl_exporter.export_cull = bool(options.get('export_cull', False)) - ipl_exporter.export_grge = bool(options.get('export_grge', False)) - ipl_exporter.export_enex = bool(options.get('export_enex', False)) - - ipl_exporter.x_offset = float(options.get('x_offset', 0.0)) - ipl_exporter.y_offset = float(options.get('y_offset', 0.0)) - ipl_exporter.z_offset = float(options.get('z_offset', 0.0)) - - ipl_exporter.export_ipl(options['file_name']) -####################################################### -class ide_exporter: - """Export an Item Definition file""" - only_selected = False - game_id = None - export_objs = True - export_tobj = False - export_anim = False - - objs = [] - tobj = [] - anim = [] - total_definitions_num = 0 - - ####################################################### - @staticmethod - def get_prop(obj, name, cast=None, fallback=None): - dff_map = getattr(obj, "dff_map", None) - val = None - if dff_map and hasattr(dff_map, name): - val = getattr(dff_map, name) - elif name in obj.keys(): - val = obj[name] - if val is None: - return fallback - try: - return cast(val) if cast else val - except Exception: - return fallback - - ####################################################### - @staticmethod - def skip_lod(obj, context): - dff_scene = getattr(context.scene, "dff", None) - return bool(dff_scene and getattr(dff_scene, "skip_lod", False) and obj.name.startswith("LOD")) - ####################################################### - @staticmethod - def is_exportable(obj, context): - if ide_exporter.only_selected and not obj.select_get(): - return False - if obj.type != "MESH": # skip empties, armatures, etc. - return False - if ".ColMesh" in obj.name: # skip collisions - return False - if obj.name != obj.name.split('.')[0]: - return False - dff_tag = getattr(obj, "dff", None) - if not (dff_tag and getattr(dff_tag, "type", None) == "OBJ"): - return False - if ide_exporter.skip_lod(obj, context): - return False - return True - - ####################################################### - @staticmethod - def draw_distances(obj): - s = ide_exporter.get_prop(obj, "ide_draw_distances", str) - if s: - vals = [v.strip() for v in s.split(",")] - return [float(v) for v in vals if v] - vals = [] - for k in ("ide_draw_distance", "ide_draw_distance1", "ide_draw_distance2", "ide_draw_distance3"): - v = ide_exporter.get_prop(obj, k, float) - if v is not None: - vals.append(v) - return vals or [100.0] - ####################################################### - @staticmethod - def count_obj_mesh_parts(root): - count = 0 - stack = [root] - seen = set() - while stack: - o = stack.pop() - if o in seen: - continue - seen.add(o) - if ( - o.type == "MESH" - and getattr(getattr(o, "dff", None), "type", None) == "OBJ" - and ".ColMesh" not in o.name - ): - count += 1 - stack.extend(list(o.children)) - return max(1, count) - - ####################################################### - @staticmethod - def collect_objs(context): - self = ide_exporter - self.objs, self.tobj, self.anim = [], [], [] - - for obj in context.scene.objects: - if not self.is_exportable(obj, context): - continue - section = (self.get_prop(obj, "ide_section", str, "objs") or "objs").lower() - if section == "objs" and self.export_objs: - self.objs.append(obj) - elif section == "tobj" and self.export_tobj: - self.tobj.append(obj) - elif section == "anim" and self.export_anim: - self.anim.append(obj) - - self.total_definitions_num = len(self.objs) + len(self.tobj) + len(self.anim) - - ####################################################### - @staticmethod - def fmt_objs(obj): - model_id = ide_exporter.get_prop(obj, "ide_object_id", int, - ide_exporter.get_prop(obj, "object_id", int)) - model_name = ide_exporter.get_prop(obj, "ide_model_name", str, - ide_exporter.get_prop(obj, "model_name", str, obj.name)) - txd_name = ide_exporter.get_prop(obj, "ide_txd_name", str, model_name) - flags = ide_exporter.get_prop(obj, "ide_flags", int, 0) - draws = ide_exporter.draw_distances(obj) - - if model_id is None or not model_name or not txd_name or not draws: - return None - - # Get type - t = ide_exporter.get_prop(obj, "ide_type", int, 0) - if t not in (1, 2, 3, 4): - gid = str(ide_exporter.game_id or "").lower() - t = 4 if ("san" in gid or "sa" == gid or " san " in gid) else 1 - - mesh_count = ide_exporter.get_prop(obj, "ide_meshes", int, None) - if t in (1, 2, 3): - if mesh_count is None or mesh_count <= 0: - mesh_count = t - - # from GTAMods - if t == 4: - d1 = float(draws[0]) - return f"{model_id}, {model_name}, {txd_name}, {d1:.0f}, {flags}" - - if t == 1: - d1 = float(draws[0]) - return f"{model_id}, {model_name}, {txd_name}, {mesh_count}, {d1:.0f}, {flags}" - - if t == 2: - d1 = float(draws[0]); d2 = float(draws[1] if len(draws) > 1 else d1) - return f"{model_id}, {model_name}, {txd_name}, {mesh_count}, {d1:.0f}, {d2:.0f}, {flags}" - - # t == 3 - d1 = float(draws[0]) - d2 = float(draws[1] if len(draws) > 1 else d1) - d3 = float(draws[2] if len(draws) > 2 else d1) - return f"{model_id}, {model_name}, {txd_name}, {mesh_count}, {d1:.0f}, {d2:.0f}, {d3:.0f}, {flags}" - ####################################################### - @staticmethod - def fmt_tobj(obj): - base = ide_exporter.fmt_objs(obj) - if not base: - return None - on_ = ide_exporter.get_prop(obj, "ide_time_on", int) - off_ = ide_exporter.get_prop(obj, "ide_time_off", int) - if on_ is None or off_ is None: - return None - return f"{base}, {on_}, {off_}" - ####################################################### - @staticmethod - def fmt_anim(obj): - base = ide_exporter.fmt_objs(obj) - if not base: - return None - anim = ide_exporter.get_prop(obj, "ide_anim", str) - if not anim: - return None - return f"{base}, {anim}" - - ####################################################### - @staticmethod - def write_section(fh, name, objects, formatter): - lines = [] - for o in objects: - s = formatter(o) - if s: - lines.append(s) - if lines: - SectionUtility(name).write(fh, lines) - ####################################################### - @staticmethod - def export_ide(filename, context=None): - self = ide_exporter - context = context or bpy.context - self.collect_objs(context) - - folder = os.path.dirname(filename) - if folder and not os.path.exists(folder): - os.makedirs(folder, exist_ok=True) - - with open(filename, "w", encoding="ascii", errors="replace", newline="\n") as fh: - fh.write("# IDE generated with DragonFF\n\n") - self.write_section(fh, "objs", self.objs, self.fmt_objs) - self.write_section(fh, "tobj", self.tobj, self.fmt_tobj) - self.write_section(fh, "anim", self.anim, self.fmt_anim) -####################################################### -def export_ide(options): - """IDE export functions""" - ide_exporter.only_selected = bool(options.get('only_selected', False)) - ide_exporter.game_id = options.get('game_id', None) - ide_exporter.export_objs = bool(options.get('export_objs', True)) - ide_exporter.export_tobj = bool(options.get('export_tobj', False)) - ide_exporter.export_anim = bool(options.get('export_anim', False)) - ide_exporter.export_ide(options['file_name']) -####################################################### -class pwn_exporter: - """Export a Pawn script""" - only_selected = False - game_id = None - stream_distance = 300.0 - draw_distance = 300.0 - x_offset = 0.0 - y_offset = 0.0 - z_offset = 0.0 - - write_artconfig = True - model_directory = "" - base_id = 19379 - id_start = -1000 - id_min = -40000 - - inst_reps = [] - total_objects_num = 0 - - ####################################################### - @staticmethod - def _skip_lod(obj, context): - dff = getattr(context.scene, "dff", None) - if not dff: - return False - if not getattr(dff, "skip_lod", False): - return False - n = obj.name - return n.startswith("LOD") or ".ColMesh" in n - - ####################################################### - @staticmethod - def _is_obj_mesh(obj): - if obj.type != "MESH": - return False - if ".ColMesh" in obj.name: - return False - dff_tag = getattr(obj, "dff", None) - return bool(dff_tag and getattr(dff_tag, "type", None) == "OBJ") - - ####################################################### - @staticmethod - def _root_anchor(obj): - a = obj - while a.parent and a.parent.type == "EMPTY": - a = a.parent - return a - - ####################################################### - @staticmethod - def collect_instances(context): - self = pwn_exporter - self.inst_reps = [] - seen = set() - for obj in context.scene.objects: - if self.only_selected and not obj.select_get(): - continue - if not self._is_obj_mesh(obj): - continue - if self._skip_lod(obj, context): - continue - root = self._root_anchor(obj) - key = root.as_pointer() - if key in seen: - continue - seen.add(key) - self.inst_reps.append(obj) - self.total_objects_num = len(self.inst_reps) - - ####################################################### - @staticmethod - def _inst_transform(obj): - parent = obj.parent - if parent and parent.type == 'EMPTY': - loc = parent.location - rx, ry, rz = euler_xyz(parent) - else: - loc = obj.location - rx, ry, rz = euler_xyz(obj) - x = loc.x + pwn_exporter.x_offset - y = loc.y + pwn_exporter.y_offset - z = loc.z + pwn_exporter.z_offset - return x, y, z, (rx, ry, rz) - - ####################################################### - @staticmethod - def _model_meta(obj): - dm = getattr(obj, "dff_map", None) - - model_id = int(getattr(dm, "object_id", 0)) if dm else 0 - interior = int(getattr(dm, "interior", -1)) if dm else -1 - - base_name = obj.name.split('.')[0] - - modelname = (getattr(dm, "model_name", None) or base_name) if dm else base_name - - dff_name = ( - (getattr(dm, "pawn_model_name", None) or getattr(dm, "ide_model_name", None)) if dm else None - ) or obj.get('DFF_Name', base_name) - - txd_name = ( - (getattr(dm, "pawn_txd_name", None) or getattr(dm, "ide_txd_name", None)) if dm else None - ) or obj.get('TXD_Name', base_name) - - return model_id, interior, modelname, dff_name, txd_name - ####################################################### - @staticmethod - def export_pawn(filename): - self = pwn_exporter - ctx = bpy.context - self.collect_instances(ctx) - if not self.total_objects_num: - return - - folder = os.path.dirname(filename) - if folder and not os.path.exists(folder): - os.makedirs(folder, exist_ok=True) - artconfig_path = os.path.join(folder, "artconfig.txt") - - next_id = self.id_start - name_to_id = {} - - with open(filename, "w", encoding="ascii", errors="replace", newline="\n") as fpwn, \ - open(artconfig_path, "w", encoding="ascii", errors="replace", newline="\n") as facfg: - - fpwn.write("// Pawn generated with DragonFF\n") - fpwn.write("// Objects: %d\n\n" % self.total_objects_num) - if self.write_artconfig: - facfg.write("// artconfig generated with DragonFF\n") - - for obj in self.inst_reps: - base_name = obj.name.split('.')[0] - if base_name not in name_to_id: - model_new_id = next_id - next_id -= 1 - if next_id <= self.id_min: - next_id -= 1000 - name_to_id[base_name] = model_new_id - else: - model_new_id = name_to_id[base_name] - - x, y, z, (rx, ry, rz) = self._inst_transform(obj) - _orig, interior, modelname, dff_name, txd_name = self._model_meta(obj) - - fpwn.write( - f"CreateDynamicObject({model_new_id}, {x:.3f}, {y:.3f}, {z:.3f}, " - f"{rx:.3f}, {ry:.3f}, {rz:.3f}, -1, {interior}, -1, " - f"{self.stream_distance:.3f}, {self.draw_distance:.3f}); // {modelname}\n" - ) - - if 'LODIndex' in obj: - lod_index = int(obj['LODIndex']) - fpwn.write( - f"CreateDynamicObject({lod_index}, {x:.3f}, {y:.3f}, {z:.3f}, " - f"{rx:.3f}, {ry:.3f}, {rz:.3f}, -1, {interior}, -1, " - f"{self.stream_distance:.3f}, {self.draw_distance:.3f}); // LOD for {modelname}\n" - ) - - if self.write_artconfig: - prefix = self.model_directory.strip().replace("\\", "/") - if prefix and not prefix.endswith("/"): - prefix += "/" - facfg.write( - f"AddSimpleModel(-1, {self.base_id}, {model_new_id}, " - f"\"{prefix}{dff_name}.dff\", \"{prefix}{txd_name}.txd\"); // {modelname}\n" - ) - -####################################################### -def export_pawn(options): - """Pawn export functions""" - pwn_exporter.only_selected = bool(options.get('only_selected', False)) - pwn_exporter.game_id = options.get('game_id', None) - pwn_exporter.stream_distance = float(options.get('stream_distance', 300.0)) - pwn_exporter.draw_distance = float(options.get('draw_distance', 300.0)) - pwn_exporter.x_offset = float(options.get('x_offset', 0.0)) - pwn_exporter.y_offset = float(options.get('y_offset', 0.0)) - pwn_exporter.z_offset = float(options.get('z_offset', 0.0)) - - pwn_exporter.write_artconfig = bool(options.get('write_artconfig', True)) - pwn_exporter.model_directory = str(options.get('model_directory', "") or "") - pwn_exporter.base_id = int(options.get('base_id', 19379)) - pwn_exporter.id_start = int(options.get('id_start', -1000)) - pwn_exporter.id_min = int(options.get('id_min', -40000)) - - pwn_exporter.export_pawn(options['file_name']) \ No newline at end of file From e4c9cf6cdbbdcac778baec640da445737dfc7dcd Mon Sep 17 00:00:00 2001 From: Psycrow Date: Sun, 31 Aug 2025 00:43:53 +0300 Subject: [PATCH 5/6] Removed outdated functions --- gui/map_ot.py | 6 ------ ops/map_importer.py | 33 --------------------------------- 2 files changed, 39 deletions(-) diff --git a/gui/map_ot.py b/gui/map_ot.py index 7a2cbf1..198d0b4 100644 --- a/gui/map_ot.py +++ b/gui/map_ot.py @@ -187,12 +187,6 @@ def execute(self, context): else: self._col_loaded = True - if self._importer.enex_instances: - self._enex_loaded = False - self._progress_total += 1 - else: - self._enex_loaded = True - wm = context.window_manager wm.progress_begin(0, 100.0) diff --git a/ops/map_importer.py b/ops/map_importer.py index 8d283fb..bfbdada 100644 --- a/ops/map_importer.py +++ b/ops/map_importer.py @@ -34,7 +34,6 @@ class map_importer: grge_instances = [] enex_instances = [] col_files = [] - enex_instances = [] collision_collection = None object_instances_collection = None mesh_collection = None @@ -381,38 +380,6 @@ def create_entries_collection(context, postfix): return coll - ####################################################### - @staticmethod - def create_enex_collection(context): - self = map_importer - if self.settings is None: - self.settings = context.scene.dff - - coll_name = f"{self.settings.game_version_dropdown} ENEX" - - coll = self.enex_collection if map_importer.fix_id(self.enex_collection) else None - if coll is None and self.enex_collection_name: - coll = bpy.data.collections.get(self.enex_collection_name) - - if coll is None: - coll = bpy.data.collections.get(coll_name) - if coll is None: - coll = bpy.data.collections.new(coll_name) - - if coll.name not in {c.name for c in context.scene.collection.children}: - context.scene.collection.children.link(coll) - - # Optional (nice): start hidden - try: - context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[coll_name] - context.view_layer.active_layer_collection.hide_viewport = True - except Exception: - pass - - self.enex_collection = coll - self.enex_collection_name = coll.name - return coll - ####################################################### @staticmethod def load_map(settings): From a5976e0f8ea3112146ca33aa28307b8c0959cb56 Mon Sep 17 00:00:00 2001 From: Psycrow Date: Sun, 31 Aug 2025 17:45:21 +0300 Subject: [PATCH 6/6] Removed extra map_ot.py --- map_ot.py | 827 ------------------------------------------------------ 1 file changed, 827 deletions(-) delete mode 100644 map_ot.py diff --git a/map_ot.py b/map_ot.py deleted file mode 100644 index de7cce1..0000000 --- a/map_ot.py +++ /dev/null @@ -1,827 +0,0 @@ -# GTA DragonFF - Blender scripts to edit basic GTA formats -# Copyright (C) 2019 Parik - -# 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 3 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, see . - -import bpy -import math -import os -import time -import bmesh - -from bpy.props import StringProperty, CollectionProperty -from bpy_extras.io_utils import ImportHelper, ExportHelper - - -from ..ops import map_exporter, map_importer -from ..ops.cull_importer import cull_importer -from ..ops.importer_common import link_object - - -####################################################### -class SCENE_OT_dff_import_map(bpy.types.Operator): - """Tooltip""" - bl_idname = "scene.dragonff_map_import" - bl_label = "Import map section" - - _timer = None - _updating = False - _progress_current = 0 - _progress_total = 0 - _importer = None - - _inst_index = 0 - _inst_loaded = False - - _col_index = 0 - _col_loaded = True - - _cull_loaded = True - - _grge_loaded = True - _enex_loaded = True - - ####################################################### - def modal(self, context, event): - - if event.type in {'ESC'}: - self.cancel(context) - return {'CANCELLED'} - - if event.type == 'TIMER' and not self._updating: - self._updating = True - - importer = self._importer - - # Import CULL if there are any left to load - if not self._cull_loaded: - - for cull in importer.cull_instances: - importer.import_cull(context, cull) - - self._progress_current += 1 - self._cull_loaded = True - - # Import Garages if there are any left to load - elif not self._grge_loaded: - for g in getattr(importer, 'garage_instances', []): - try: - importer.import_garage(context, g) - except Exception as ex: - print("Can't import GRGE... skipping", ex) - self._progress_current += 1 - self._grge_loaded = True - - # Import Enex checkpoints if there are any left to load - elif not self._enex_loaded: - for e in self._importer.enex_instances: - try: - self._importer.import_enex(context, e) - except Exception as ex: - print("Can't import ENEX... skipping", ex) - self._progress_current += 1 - self._enex_loaded = True - - # Import collision files if there are any left to load - elif not self._col_loaded: - num_objects_at_once = 5 - cols_num = len(importer.col_files) - - for _ in range(num_objects_at_once): - if self._col_index >= cols_num: - self._col_loaded = True - break - - # Fetch next collision - col_file = importer.col_files[self._col_index] - self._col_index += 1 - - importer.import_collision(context, col_file) - self._progress_current += 1 - - # Import object instances - else: - # As the number of objects increases, loading performance starts to get crushed by scene updates, so - # we try to keep loading at least 5% of the total scene object count on each timer pulse. - num_objects_at_once = max(10, int(0.05 * len(bpy.data.objects))) - instances_num = len(importer.object_instances) - - for _ in range(num_objects_at_once): - if self._inst_index >= instances_num: - self._inst_loaded = True - break - - # Fetch next instance - inst = importer.object_instances[self._inst_index] - self._inst_index += 1 - - try: - importer.import_object_instance(context, inst) - except: - print("Can`t import model... skipping") - - self._progress_current += 1 - - # Update cursor progress indicator if something needs to be loaded - progress = ( - float(self._progress_current) / float(self._progress_total) - ) if self._progress_total else 100 - - context.window_manager.progress_update(progress) - - # Update dependency graph - dg = context.evaluated_depsgraph_get() - dg.update() - - self._updating = False - - if self._inst_loaded: - self.cancel(context) - return {'FINISHED'} - - return {'PASS_THROUGH'} - - ####################################################### - def execute(self, context): - - settings = context.scene.dff - self._importer = map_importer.load_map(settings) - - self._progress_current = 0 - self._progress_total = 0 - - self._inst_index = 0 - self._inst_loaded = False - self._progress_total += len(self._importer.object_instances) - - if self._importer.cull_instances: - self._cull_loaded = False - self._progress_total += 1 - else: - self._cull_loaded = True - - if self._importer.garage_instances: - self._grge_loaded = False - self._progress_total += 1 - else: - self._grge_loaded = True - - if self._importer.col_files: - self._col_index = 0 - self._col_loaded = False - self._progress_total += len(self._importer.col_files) - else: - self._col_loaded = True - - if self._importer.enex_instances: - self._enex_loaded = False - self._progress_total += 1 - else: - self._enex_loaded = True - - wm = context.window_manager - wm.progress_begin(0, 100.0) - - # Call the "modal" function every 0.1s - self._timer = wm.event_timer_add(0.1, window=context.window) - wm.modal_handler_add(self) - - return {'RUNNING_MODAL'} - - ####################################################### - def cancel(self, context): - wm = context.window_manager - wm.progress_end() - wm.event_timer_remove(self._timer) - -####################################################### -class SCENE_OT_ipl_select(bpy.types.Operator, ImportHelper): - - bl_idname = "scene.select_ipl" - bl_label = "Select IPL File" - - filename_ext = ".ipl" - - filter_glob : bpy.props.StringProperty( - default="*.ipl", - options={'HIDDEN'}) - - def invoke(self, context, event): - self.filepath = context.scene.dff.game_root + "/data/maps/" - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - def execute(self, context): - if os.path.splitext(self.filepath)[-1].lower() == self.filename_ext: - filepath = os.path.normpath(self.filepath) - # Try to find if the file is within the game directory structure - sep_pos = filepath.upper().find(f"data{os.sep}maps") - - if sep_pos != -1: - # File is within game directory, use relative path - game_root = filepath[:sep_pos] - context.scene.dff.game_root = game_root - context.scene.dff.custom_ipl_path = os.path.relpath(filepath, game_root) - else: - # File is outside game directory, use absolute path - # Don't change game_root, keep the existing one - context.scene.dff.custom_ipl_path = filepath - return {'FINISHED'} - -####################################################### -class EXPORT_OT_ipl(bpy.types.Operator, ExportHelper): - bl_idname = "export_scene.dff_ipl" - bl_label = "DragonFF IPL Export" - bl_description = "Export a GTA IPL file with INST, CULL, or both sections" - filename_ext = ".ipl" - - export_inst: bpy.props.BoolProperty( - name="Export INST (object placements)", - default=True, - description="Export object placement (INST) section" - ) - export_cull: bpy.props.BoolProperty( - name="Export CULL zones", - default=True, - description="Export CULL (zone) section" - ) - - export_grge: bpy.props.BoolProperty( - name="Export GRGE zones", - default=True, - description="Export GRGE (zone) section" - ) - - export_enex: bpy.props.BoolProperty( - name="Export ENEX zones", - default=True, - description="Export ENEX (zone) section" - ) - - only_selected: bpy.props.BoolProperty( - name="Only Selected", - default=False - ) - 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" - ) - - 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="*.ipl", - options={'HIDDEN'} - ) - - ####################################################### - def draw(self, context): - layout = self.layout - layout.prop(self, "export_inst") - layout.prop(self, "export_cull") - layout.prop(self, "export_grge") - layout.prop(self, "export_enex") - layout.prop(self, "only_selected") - 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): - start = time.time() - try: - export_inst = self.export_inst - export_cull = self.export_cull - export_grge = self.export_grge - export_enex = self.export_enex - map_exporter.export_ipl( - { - "file_name": self.filepath, - "only_selected": self.only_selected, - "game_id": context.scene.dff.game_version_dropdown, - "export_inst": export_inst, - "export_cull": export_cull, - "export_grge": export_grge, - "export_enex": export_enex, - "x_offset": self.x_offset, - "y_offset": self.y_offset, - "z_offset": self.z_offset, - } - ) - - if not map_exporter.ipl_exporter.total_objects_num: - self.report({"ERROR"}, "No objects with IPL data found") - return {'CANCELLED'} - - self.report({"INFO"}, f"Finished export in {time.time() - start:.2f}s") - - except Exception as e: - self.report({"ERROR"}, str(e)) - return {'CANCELLED'} - - 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): - - bl_idname = "object.dff_add_cull" - bl_label = "Add CULL Zone" - bl_description = "Add CULL zone to the scene" - bl_options = {'REGISTER', 'UNDO'} - - location: bpy.props.FloatVectorProperty( - name="Location", - description="Location for the newly added object", - subtype='XYZ', - default=(0, 0, 0) - ) - - scale: bpy.props.FloatVectorProperty( - name="Scale", - description="Scale for the newly added object", - subtype='XYZ', - default=(1, 1, 1) - ) - - angle: bpy.props.FloatProperty( - name="Angle", - description="Angle along the Z axis", - subtype='ANGLE', - min=-math.pi * 2, - max=math.pi * 2, - step=100, - default=0 - ) - - ####################################################### - def invoke(self, context, event): - self.location = context.scene.cursor.location - return self.execute(context) - - ####################################################### - def execute(self, context): - obj = cull_importer.create_cull_object( - location=self.location, - scale=self.scale, - flags=0, - angle=self.angle - ) - link_object(obj, context.collection) - - context.view_layer.objects.active = obj - for o in context.selected_objects: - o.select_set(False) - obj.select_set(True) - - return {'FINISHED'} - -####################################################### -class OBJECT_OT_dff_add_enex(bpy.types.Operator): - bl_idname = "object.dff_add_enex" - bl_label = "Add ENEX Zone" - bl_description = "Add an ENEX (entry/exit) marker to the scene (wireframe cylinder, size 1.24)" - bl_options = {'REGISTER', 'UNDO'} - - location: bpy.props.FloatVectorProperty( - name="Location", - description="Location for the newly added ENEX marker", - subtype='XYZ', - default=(0.0, 0.0, 0.0) - ) - angle: bpy.props.FloatProperty( - name="Angle", - description="Angle around Z (radians)", - subtype='ANGLE', - default=0.0 - ) - name_hint: bpy.props.StringProperty( - name="Name", - description="Optional ENEX name", - default="ENEX" - ) - ####################################################### - def invoke(self, context, event): - self.location = context.scene.cursor.location - return self.execute(context) - ####################################################### - def execute(self, context): - MapImporter = map_importer.map_importer - if getattr(MapImporter, "settings", None) is None: - MapImporter.settings = context.scene.dff - - coll = MapImporter.create_enex_collection(context) - - try: - me = MapImporter.create_enex_cylinder() - except AttributeError: - me = bpy.data.meshes.new("_ENEX_") - bm = bmesh.new() - bmesh.ops.create_cone( - bm, segments=24, - radius1=1.24, radius2=1.24, depth=1.24, - cap_ends=True, cap_tris=False - ) - bm.to_mesh(me); bm.free() - - obj = bpy.data.objects.new( - f"ENEX_{self.name_hint}" if self.name_hint else "ENEX", me - ) - obj.location = self.location - obj.rotation_mode = 'ZXY' - obj.rotation_euler = (0.0, 0.0, float(self.angle)) - - obj.hide_render = True - try: - obj.display_type = 'WIRE' - except Exception: - pass - - mat = bpy.data.materials.get("_ENEX") or bpy.data.materials.new("_ENEX") - mat.diffuse_color = (1.0, 0.85, 0.10, 1.0) - if not obj.data.materials: - obj.data.materials.append(mat) - else: - obj.data.materials[0] = mat - - if hasattr(obj, "dff"): - obj.dff.type = "ENEX" - if hasattr(obj, "dff_map"): - obj.dff_map.ipl_section = "enex" - else: - obj["ipl_section"] = "enex" - - obj["enex_name"] = self.name_hint - obj["enex_posX"] = float(obj.location.x) - obj["enex_posY"] = float(obj.location.y) - obj["enex_posZ"] = float(obj.location.z) - obj["enex_rotZ"] = float(self.angle) - - link_object(obj, coll) - - context.view_layer.objects.active = obj - for o in context.selected_objects: - o.select_set(False) - 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