From 7ae9cfd3e5fb6d9a0101652d0061406bc985b369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Fut=C3=A1sz?= <34510704+bfut@users.noreply.github.com> Date: Wed, 7 Aug 2024 22:33:37 +0200 Subject: [PATCH] version 3.3 requires unvivtool 3.0 --- README.md | 19 +++- fcecodec_blender.py | 251 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 242 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index a627f91..4cf3aa6 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,26 @@ Supports Blender 4.x, 4.2 LTS, and 3.6 LTS on Windows, Linux, and macOS. - alternatively, select (.viv) archive 1. push "Select from (.viv)" button 2. Select (.fce) file and optional (.tga) from the lists + 3. Hit "Import FCE" button * File > Export > Need For Speed (.fce) - - self-explaining Blender export dialog + - export as (.fce) file + - alternatively, select (.viv) archive + 1. push "Select from (.viv)" button + 2. Select (.fce) file from the list + 3. Hit "Export FCE" button + +## Tutorial + +The following tutorial shows how to use Blender to: +* create/modify damage models +* set part centers +* set vertice animation flags +* edit dummies (light / fx objects) +* edit triangle flags +* edit texture pages + +[Tutorial](https://github.com/bfut/fcecodec/tree/main/scripts/doc_Obj2Fce.md) ## Information __License:__ GNU General Public License v3.0+
diff --git a/fcecodec_blender.py b/fcecodec_blender.py index fb039e9..9d9d03c 100644 --- a/fcecodec_blender.py +++ b/fcecodec_blender.py @@ -27,15 +27,31 @@ - alternatively, select (.viv) archive 1. push "Select from (.viv)" button 2. Select (.fce) file and optional (.tga) from the lists + 3. Hit "Import FCE" button * File > Export > Need For Speed (.fce) - - self-explaining Blender export dialog + - export as (.fce) file + - alternatively, select (.viv) archive + 1. push "Select from (.viv)" button + 2. Select (.fce) file from the list + 3. Hit "Export FCE" button + +TUTORIAL: + The following tutorial shows how to use Blender to: + * create/modify damage models + * set part centers + * set vertice animation flags + * edit dummies (light / fx objects) + * edit triangle flags + * edit texture pages + + https://github.com/bfut/fcecodec/tree/main/scripts/doc_Obj2Fce.md """ bl_info = { "name": "fcecodec_blender", "author": "Benjamin Futasz", - "version": (3, 2), + "version": (3, 3), "blender": (3, 6, 0), "location": "File > Import/Export > Need For Speed (.fce)", "description": "Imports & Exports Need For Speed (.fce) files, powered by fcecodec", @@ -74,7 +90,7 @@ def pip_install(package, upgrade=False, pre=False, version: str | None = None): pip_install("fcecodec", upgrade=True, version=min_fcecodec_version) import fcecodec as fc -min_unvivtool_version = "2.2" +min_unvivtool_version = "3.0" try: import unvivtool as uvt if uvt.__version__ < min_fcecodec_version: @@ -1086,28 +1102,28 @@ def get_fce_tga_list_from_viv(vd: dict): 'validity_bitmap': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]} """ if vd.get("format", None) != "BIGF": - return False, None, None + return False, None, None, False files = np.array(vd.get("files", [])) if len(files) < 1: - return False, None, None + return False, None, None, False validity_bitmap = np.array(vd.get("validity_bitmap", [])) fce_idx = np.where([f.lower().endswith(".fce") for f in files])[0] fce_idx = fce_idx[validity_bitmap[fce_idx] == 1] # only valid files if len(fce_idx) < 1: - return False, None, None + return False, None, None, True tga_idx = np.where([f.lower().endswith(".tga") for f in files])[0] tga_idx = tga_idx[validity_bitmap[tga_idx] == 1] # only valid files - return True, fce_idx, tga_idx + return True, fce_idx, tga_idx, True - ret, fce_idx, tga_idx = get_fce_tga_list_from_viv(vd) + ret, fce_idx, tga_idx, valid = get_fce_tga_list_from_viv(vd) if not ret: - return False, None, None, None, None, None + return False, None, None, None, None, None, valid files = np.array(vd.get("files", [])) # files_offsets = np.array(vd.get("files_offsets", [])) # files_sizes = np.array(vd.get("files_sizes", [])) # validity_bitmap = np.array(vd.get("validity_bitmap", [])) - return True, files[fce_idx], files[tga_idx], vd, fce_idx, tga_idx + return True, files[fce_idx], files[tga_idx], vd, fce_idx, tga_idx, valid ######################################################### @@ -1126,7 +1142,7 @@ class FCEC_OT_UpdateUIList(Operator): filepath: StringProperty(default="") def decode(self, context, fp: pathlib.Path): - ret, fce_files, tga_files, _, _, _ = unvivtool_integration(fp) + ret, fce_files, tga_files, _, _, _, _ = unvivtool_integration(fp) if not ret: return valid_files = np.concatenate((fce_files, tga_files)) @@ -1140,7 +1156,6 @@ def add_value(self, context, val: str): fce_files = context.scene.fce_files item = fce_files.add() item.name = val - # item.vivpath = pathlib.Path(self.filepath) item.vivpath = self.filepath elif ext.lower() == ".tga": tga_files = context.scene.tga_files @@ -1160,6 +1175,67 @@ def execute(self, context): self.filepath = "" # avoid decoder loop return {'FINISHED'} +class FCEC_OT_UpdateUIListExport(Operator): + """Update fce and tga UI lists.""" + bl_idname = "viv_files_export.update" + bl_label = "Update fce and tga lists" + + filepath: StringProperty(default="") + + def decode(self, context, fp: pathlib.Path): + ret, fce_files, tga_files, _, _, _, valid = unvivtool_integration(fp) + if not ret and not valid: + return + elif ret: + valid_files = np.concatenate((fce_files, tga_files)) + print(f"valid_files: {valid_files}") + for v in valid_files: + self.add_value(context, v) + if valid: + self.add_message(context, "foobar.fce", "") + self.add_message(context, "foobar.tga", "") + + def add_value(self, context, val: str): + ext = pathlib.Path(val).suffix + if ext.lower() == ".fce": + fce_files = context.scene.fce_files + item = fce_files.add() + item.name = val + item.vivpath = self.filepath + elif ext.lower() == ".tga": + tga_files = context.scene.tga_files + item = tga_files.add() + item.name = val + item.vivpath = self.filepath + else: + print(f"Unknown file extension '{ext}'") + + def add_message(self, context, val: str, msg: str): + ext = pathlib.Path(val).suffix + if ext.lower() == ".fce": + fce_files = context.scene.fce_files + item = fce_files.add() + item.name = msg + # item.vivpath = pathlib.Path(self.filepath) + item.vivpath = self.filepath + elif ext.lower() == ".tga": + tga_files = context.scene.tga_files + item = tga_files.add() + item.name = msg + item.vivpath = self.filepath + else: + print(f"Unknown file extension '{ext}'") + + def execute(self, context): + context.scene.fce_files.clear() # reset list + context.scene.tga_files.clear() # reset list + fp = pathlib.Path(self.filepath) + if fp.suffix.lower() == ".viv": + print(f"Decoding archive... '{fp}'") + self.decode(context, fp) + self.filepath = "" # avoid decoder loop + return {'FINISHED'} + class FCEC_UL_stringlist(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): ob = data @@ -1276,9 +1352,7 @@ def draw(self, context): break else: self.current_num_fce_files = 0 - sce = context.scene - row = layout.row() if pathlib.Path(self.current_viv_archive).suffix.lower() == ".viv": row.operator("viv_files.update", text="Select from (.viv)").filepath = self.current_viv_archive @@ -1317,7 +1391,7 @@ def execute(self, context): fp = pathlib.Path(f.name) if fp.suffix.lower() == ".viv": fp = pdir / fp - ret, _, _, vd, fce_idx, _ = unvivtool_integration(fp) + ret, _, _, vd, fce_idx, _, valid = unvivtool_integration(fp) if not ret: continue if len(fce_idx) > 0: @@ -1461,8 +1535,6 @@ def decode_viv(vivpath, path, texname, tempfiles, vd, tdir, time_suffix): # cleanup if not self.addon_dev_mode: - # path_obj.unlink() - # path_mtl.unlink() for f in tempfiles: f = pathlib.Path(f).unlink() @@ -1476,7 +1548,7 @@ class FcecodecExport(Operator, ExportHelper): filename_ext = "" - filter_glob: StringProperty(default="*.fce", options={"HIDDEN"}) + filter_glob: StringProperty(default="*.fce;*.viv", options={"HIDDEN"}) addon_dev_mode: BoolProperty( name="Developer Mode", @@ -1484,6 +1556,11 @@ class FcecodecExport(Operator, ExportHelper): default=False ) + files: CollectionProperty( + name="File Path", + type=bpy.types.OperatorFileListElement, + ) + export_selected_objects: BoolProperty( name="Limit export to selected objects", description="Ignore parts that are not selected. ", @@ -1584,6 +1661,10 @@ class FcecodecExport(Operator, ExportHelper): default=False ) + tga_path: StringProperty( + subtype="FILE_PATH" + ) + # draws export dialog def draw(self, context): @@ -1618,18 +1699,84 @@ def draw(self, context): box = layout.box() box.prop(self, "fce_normals2vertices") + # if VIV selected in import dialog, display contents of first VIV file + if self.files: + for f in self.files: + fp = pathlib.Path(f.name) + if fp.suffix.lower() == ".viv": + pdir = pathlib.Path(self.filepath).parent + fp = pdir / fp + if not fp.is_file() or self.current_viv_archive == fp: # avoid decoder loop + break + self.current_viv_archive = str(fp) + break + else: + self.current_num_fce_files = 0 + sce = context.scene + row = layout.row() + if pathlib.Path(self.current_viv_archive).suffix.lower() == ".viv": + row.operator("viv_files.update", text="Select in (.viv)").filepath = self.current_viv_archive + # row.operator("viv_files_export.update", text="Select in (.viv)").filepath = self.current_viv_archive + self.current_viv_archive = "" # avoid decoder loop + layout.template_list("FCEC_UL_stringlist", "", sce, "fce_files", sce, "active_fce_files_index") + # layout.template_list("FCEC_UL_stringlist", "", sce, "tga_files", sce, "active_tga_files_index") + if DEV_MODE: layout.prop(self, "addon_dev_mode") + current_viv_archive = "" # avoid decoder loop def execute(self, context): - path = pathlib.Path(self.filepath) - if path.suffix.lower() != ".fce": - path = path.with_suffix(".fce") - - # paths to temporary files time_suffix = str(time.time()) tdir = pathlib.Path(tempfile.gettempdir()) + pdir = pathlib.Path(self.filepath).parent + + vivpath = None + path = None + texname = "" + tempfiles = [] # will be unlinked + if self.files: + # if VIV selected in dialog + for f in self.files: + fp = pathlib.Path(f.name) + if fp.suffix.lower() == ".viv": + fp = pdir / fp + ret, _, _, vd, fce_idx, _, valid = unvivtool_integration(fp) + if not ret: + continue + if len(fce_idx) > 0: + path = fp.with_suffix(".fce") + vivpath = fp + break + + # if at least 1 FCE selected in dialog + for f in self.files: + fp = pathlib.Path(f.name) + if fp.suffix.lower() == ".fce": + path = pdir / fp + vivpath = None # fce takes precedence + break + + # if no file selected in dialog + if not path and not vivpath: + path = pathlib.Path(self.filepath) + path = path.with_suffix(".fce") + + print(f"path: {path}") + print(f"texname: {texname}") + print(f"vivpath: {vivpath}") + + + # if vivpath is not None: copy VIV archive to temp dir, create temp FCE, modify VIV archive in temp dir, move VIV to destination path, unlink temp FCE + # else: create temp FCE, move to actual path + + + path_actual = None + if vivpath is None: path_actual = str(path) + + # paths to temporary files + path = pathlib.Path(path.stem + "_" + time_suffix).with_suffix(".fce") + path = tdir / path path_obj = pathlib.Path(path.stem + "_" + time_suffix).with_suffix(".obj") path_mtl = pathlib.Path(path.stem + "_" + time_suffix).with_suffix(".mtl") path_obj = pathlib.Path(str(path_obj).replace(" ", "_")) @@ -1637,6 +1784,16 @@ def execute(self, context): path_obj = tdir / path_obj path_mtl = tdir / path_mtl + tempfiles.append(path_obj) + tempfiles.append(path_mtl) + if vivpath: tempfiles.append(path) + + print(f"path_actual: {path_actual}") + print(f"path: {path}") + print(f"texname: {texname}") + print(f"vivpath: {vivpath}") + + # https://docs.blender.org/api/current/bpy.ops.wm.html#bpy.ops.wm.obj_export bpy.ops.wm.obj_export(filepath=str(path_obj), forward_axis=self.obj_forward_axis, @@ -1756,10 +1913,51 @@ def execute(self, context): PrintFceInfo(path) print(f"Applying options to '{path.name}' took {ptn:.2f} ms") + + # if VIV selected in export dialog, updated selected VIV with exported FCE + if vivpath: + def update_viv(vivpath: pathlib.Path, fce_path): + # check that active files from UILists are valid in selected VIV archive + sce = context.scene + if len(sce.fce_files) < 1: + print("Warning: No FCE file selected from VIV archive") + return None, None, tempfiles + + files = np.array(vd.get("files", [])) + print(f"files: {files}") + active_fce = sce.fce_files[sce.active_fce_files_index].name + print(f"active_fce: {active_fce}") + active_fce_idx = np.where(files == active_fce) + if len(active_fce_idx) < 1 or len(active_fce_idx[0]) < 1: + print("Warning: Selected FCE file not found in selected VIV archive") + return None, None, tempfiles + active_fce_idx = int(active_fce_idx[0][0]) + + print(f"active_fce_idx: {active_fce_idx}") + + # update active FCE in selected VIV archive + ptn = time.process_time_ns() + uvt.update(vivpath, fce_path, active_fce_idx+1, replace_filename=False, verbose=True) + ptn = float(time.process_time_ns() - ptn) / 1e6 + print(f"Updating '{vivpath.name}' took {ptn:.2f} ms") + + viv = None + viv = dict(uvt.get_info(vivpath)) + print(viv) + + print(f"Updating archive {vivpath}") + update_viv(vivpath, path) + + + # move temp file to destination + if vivpath is None: + if path_actual.is_file(): path_actual.unlink() + path = path.rename(path_actual) + # cleanup if not self.addon_dev_mode: - path_obj.unlink() - path_mtl.unlink() + for f in tempfiles: + f = pathlib.Path(f).unlink() return {"FINISHED"} @@ -1775,6 +1973,7 @@ def menu_func_export(self, context): FcecodecExport, FCEC_UL_stringlist, FCEC_OT_UpdateUIList, + FCEC_OT_UpdateUIListExport, VivItem, ) @@ -1783,8 +1982,6 @@ def register(): bpy.utils.register_class(cls) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.TOPBAR_MT_file_export.append(menu_func_export) - # bpy.types.Scene.viv_files = CollectionProperty(type=VivItem) # deprecated - # bpy.types.Scene.active_viv_files_index = IntProperty(name="active_viv_files_index") # deprecated bpy.types.Scene.fce_files = CollectionProperty(type=VivItem) bpy.types.Scene.active_fce_files_index = IntProperty(name="active_fce_files_index") bpy.types.Scene.tga_files = CollectionProperty(type=VivItem)