Fix Blender 5.0 compatibility and add console logging for pack operations

Guard DNA field accesses with has_field() checks for fields renamed or
removed across Blender versions (proxy, dup_group, texcomesh, nodetree,
etc.), fix mesh block pointer mismatch in path rewriting, fix mkrelative
IndexError when asset path is shorter than blend file path, and add
[BAT] console prints for tracing progress.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph HENRY 2026-03-20 15:40:12 +01:00
parent 4085f0015f
commit f79c6a276d
7 changed files with 57 additions and 16 deletions

View File

@ -75,7 +75,7 @@ class BlendPath(bytes):
# Remove matching initial parts. What is left in bdir_parts represents
# the number of '..' we need. What is left in asset_parts represents
# what we need after the '../../../'.
while bdir_parts:
while bdir_parts and asset_parts:
if bdir_parts[0] != asset_parts[0]:
break
bdir_parts.popleft()

View File

@ -30,8 +30,12 @@ class ExportBatPack(Operator, ExportHelper):
Path(bpy.data.filepath).parent,
str(self.filepath),
) as packer:
print("[BAT] Strategising (tracing dependencies)...")
packer.strategise()
print(f"[BAT] Found {len(packer._actions)} assets to process")
print("[BAT] Executing (rewriting paths and copying files)...")
packer.execute()
print("[BAT] Packing complete!")
self.report({"INFO"}, "Packing successful!")
with zipfile.ZipFile(str(self.filepath)) as inzip:
@ -123,8 +127,12 @@ class BAT_OT_export_zip(Operator, ExportHelper):
self.report({"INFO"}, "Packing with hierarchy...")
with packer_cls(bfile, project, target, keep_hierarchy=True) as packer:
print("[BAT] Strategising (tracing dependencies)...")
packer.strategise()
print(f"[BAT] Found {len(packer._actions)} assets to process")
print("[BAT] Executing (rewriting paths and copying files)...")
packer.execute()
print("[BAT] Packing complete!")
if self.use_zip:
with zipfile.ZipFile(target) as inzip:

View File

@ -64,6 +64,7 @@ def deps(
if usage_hash in seen_hashes:
continue
seen_hashes.add(usage_hash)
print(f"[BAT] Found reference: {block_usage.asset_path} (block {block.code.decode()})")
yield block_usage

View File

@ -133,14 +133,18 @@ def library(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsag
@dna_code("ME")
def mesh(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Mesh data blocks."""
block_external = block.get_pointer((b"ldata", b"external"), None)
if block_external is None:
block_external = block.get_pointer((b"fdata", b"external"), None)
block_external = None
for field_name in (b"ldata", b"fdata"):
if not block.has_field(field_name):
continue
block_external = block.get_pointer((field_name, b"external"), None)
if block_external is not None:
break
if block_external is None:
return
path, field = block_external.get(b"filename", return_field=True)
yield result.BlockUsage(block, path, path_full_field=field)
yield result.BlockUsage(block_external, path, path_full_field=field)
@dna_code("MC")

View File

@ -152,16 +152,20 @@ def _expand_generic_idprops(block: blendfile.BlendFileBlock):
def _expand_generic_nodetree_id(block: blendfile.BlendFileBlock):
if block.bfile.header.version >= 500 and block.bfile.file_subversion >= 4:
# Introduced in Blender 5.0, commit bd61e69be5a7c96f1e5da1c86aafc17b839e049f
# compositing_node_group replaced nodetree for Scene blocks in Blender 5.0.
if block.has_field(b"compositing_node_group"):
block_ntree = block.get_pointer(b"compositing_node_group", None)
else:
elif block.has_field(b"nodetree"):
block_ntree = block.get_pointer(b"nodetree", None)
else:
block_ntree = None
if block_ntree is not None:
yield from _expand_generic_nodetree(block_ntree)
def _expand_generic_animdata(block: blendfile.BlendFileBlock):
if not block.has_field(b"adt"):
return
block_adt = block.get_pointer(b"adt")
if block_adt:
yield block_adt.get_pointer(b"action")
@ -187,6 +191,7 @@ def _expand_curve(block: blendfile.BlendFileBlock):
b"taperobj",
b"textoncurve",
):
if block.has_field(fieldname):
yield block.get_pointer(fieldname)
@ -258,8 +263,9 @@ def _expand_metaball(block: blendfile.BlendFileBlock):
def _expand_mesh(block: blendfile.BlendFileBlock):
yield from _expand_generic_animdata(block)
yield from _expand_generic_material(block)
# texcomesh was removed in Blender 4.0.
if block.has_field(b"texcomesh"):
yield block.get_pointer(b"texcomesh")
# TODO, TexFace? - it will be slow, we could simply ignore :S
@dna_code("NT")
@ -275,10 +281,16 @@ def _expand_object(block: blendfile.BlendFileBlock):
yield block.get_pointer(b"data")
if block[b"transflag"] & cdefs.OB_DUPLIGROUP:
# dup_group was renamed to instance_collection in Blender 2.80.
if block.has_field(b"instance_collection"):
yield block.get_pointer(b"instance_collection", default=None)
elif block[b"transflag"] & cdefs.OB_DUPLIGROUP:
yield block.get_pointer(b"dup_group")
# proxy and proxy_group were removed in Blender 3.0.
if block.has_field(b"proxy"):
yield block.get_pointer(b"proxy")
if block.has_field(b"proxy_group"):
yield block.get_pointer(b"proxy_group")
# 'ob->pose->chanbase[...].custom'
@ -312,10 +324,17 @@ def _expand_particle_settings(block: blendfile.BlendFileBlock):
yield from _expand_generic_animdata(block)
yield from _expand_generic_mtex(block)
# dup_group/dup_ob were renamed to instance_collection/instance_object in Blender 2.80.
block_ren_as = block[b"ren_as"]
if block_ren_as == cdefs.PART_DRAW_GR:
if block.has_field(b"instance_collection"):
yield block.get_pointer(b"instance_collection")
else:
yield block.get_pointer(b"dup_group")
elif block_ren_as == cdefs.PART_DRAW_OB:
if block.has_field(b"instance_object"):
yield block.get_pointer(b"instance_object")
else:
yield block.get_pointer(b"dup_ob")

View File

@ -69,6 +69,7 @@ class BlockIterator:
"""Open a blend file, sending notification about this to the progress callback."""
log.info("opening: %s", bfilepath)
print(f"[BAT] Reading blend file: {bfilepath}")
self.progress_cb.trace_blendfile(bfilepath)
return blendfile.open_cached(bfilepath)

View File

@ -233,8 +233,12 @@ def modifier_particle_system(
def modifier_fluid_sim(
ctx: ModifierContext, modifier: blendfile.BlendFileBlock, block_name: bytes
) -> typing.Iterator[result.BlockUsage]:
"""Legacy fluid sim, removed in Blender 3.0."""
my_log = log.getChild("modifier_fluid_sim")
if not modifier.has_field(b"fss"):
return
fss = modifier.get_pointer(b"fss")
if fss is None:
my_log.debug(
@ -257,8 +261,12 @@ def modifier_fluid_sim(
def modifier_smoke_sim(
ctx: ModifierContext, modifier: blendfile.BlendFileBlock, block_name: bytes
) -> typing.Iterator[result.BlockUsage]:
"""Legacy smoke sim, removed in Blender 2.82 (replaced by Fluid modifier)."""
my_log = log.getChild("modifier_smoke_sim")
if not modifier.has_field(b"domain"):
return
domain = modifier.get_pointer(b"domain")
if domain is None:
my_log.debug(