import bpy, sys, os, re, struct, traceback ARGS_PATTERN = re.compile(r'''(?:"([^"]+)"|'([^']+)'|(\S+))''') # Background mode seems to require quit() in some 2.80 builds def _quitblender(): bpy.ops.wm.quit_blender() quit() # Extract pipe file descriptors from arguments print('HECL Blender Launch', sys.argv) if '--' not in sys.argv: _quitblender() args = sys.argv[sys.argv.index('--')+1:] readfd = int(args[0]) writefd = int(args[1]) verbosity_level = int(args[2]) err_path = "" if sys.platform == "win32": import msvcrt readfd = msvcrt.open_osfhandle(readfd, os.O_RDONLY | os.O_BINARY) writefd = msvcrt.open_osfhandle(writefd, os.O_WRONLY | os.O_BINARY) err_path = "/Temp" if 'TEMP' in os.environ: err_path = os.environ['TEMP'] else: err_path = "/tmp" if 'TMPDIR' in os.environ: err_path = os.environ['TMPDIR'] err_path += "/hecl_%016X.derp" % os.getpid() def readpipestr(): read_bytes = os.read(readfd, 4) if len(read_bytes) != 4: print('HECL connection lost or desynchronized') _quitblender() read_len = struct.unpack('I', read_bytes)[0] return os.read(readfd, read_len) def writepipestr(linebytes): #print('LINE', linebytes) os.write(writefd, struct.pack('I', len(linebytes))) os.write(writefd, linebytes) def writepipebuf(linebytes): #print('BUF', linebytes) os.write(writefd, linebytes) def quitblender(): writepipestr(b'QUITTING') _quitblender() class PathHasher: def hashpath32(self, path): writepipestr(path.encode()) read_str = readpipestr() return int(read_str[0:8], 16) # Ensure Blender 2.81 is being used if bpy.app.version < (2, 81, 0): writepipestr(b'NOT281') _quitblender() # If there's a third argument, use it as the .zip path containing the addon did_install = False if len(args) >= 4 and args[3] != 'SKIPINSTALL': bpy.ops.preferences.addon_install(overwrite=True, target='DEFAULT', filepath=args[3]) bpy.ops.preferences.addon_refresh() did_install = True # Make addon available to commands if bpy.context.preferences.addons.find('hecl') == -1: try: bpy.ops.preferences.addon_enable(module='hecl') bpy.ops.wm.save_userpref() except: pass try: import hecl except: writepipestr(b'NOADDON') _quitblender() # Quit if just installed if did_install: writepipestr(b'ADDONINSTALLED') _quitblender() # Intro handshake writepipestr(b'READY') ackbytes = readpipestr() if ackbytes != b'ACK': quitblender() # Count brackets def count_brackets(linestr): bracket_count = 0 for ch in linestr: if ch in {'[','{','('}: bracket_count += 1 elif ch in {']','}',')'}: bracket_count -= 1 return bracket_count # Read line of space-separated/quoted arguments def read_cmdargs(): cmdline = readpipestr() if cmdline == b'': print('HECL connection lost') _quitblender() cmdargs = [] for match in ARGS_PATTERN.finditer(cmdline.decode()): cmdargs.append(match.group(match.lastindex)) return cmdargs # Complete sequences of statements compiled/executed here def exec_compbuf(compbuf, globals): if verbosity_level >= 3: print(compbuf) try: co = compile(compbuf, '', 'exec') exec(co, globals) except Exception as e: trace_prefix = 'Error processing:\n' trace_prefix += compbuf raise RuntimeError(trace_prefix) from e # Command loop for writing animation key data to blender def animin_loop(globals): writepipestr(b'ANIMREADY') while True: crv_type = struct.unpack('b', os.read(readfd, 1)) if crv_type[0] < 0: writepipestr(b'ANIMDONE') return elif crv_type[0] == 0: crvs = globals['rotCurves'] elif crv_type[0] == 1: crvs = globals['transCurves'] elif crv_type[0] == 2: crvs = globals['scaleCurves'] key_info = struct.unpack('ii', os.read(readfd, 8)) crv = crvs[key_info[0]] crv.keyframe_points.add(count=key_info[1]) if crv_type[0] == 1: for k in range(key_info[1]): key_data = struct.unpack('if', os.read(readfd, 8)) pt = crv.keyframe_points[k] pt.interpolation = 'LINEAR' pt.co = (key_data[0], key_data[1]) else: for k in range(key_info[1]): key_data = struct.unpack('if', os.read(readfd, 8)) pt = crv.keyframe_points[k] pt.interpolation = 'LINEAR' pt.co = (key_data[0], key_data[1]) def writelight(obj): wmtx = obj.matrix_world writepipebuf(struct.pack('ffffffffffffffff', wmtx[0][0], wmtx[0][1], wmtx[0][2], wmtx[0][3], wmtx[1][0], wmtx[1][1], wmtx[1][2], wmtx[1][3], wmtx[2][0], wmtx[2][1], wmtx[2][2], wmtx[2][3], wmtx[3][0], wmtx[3][1], wmtx[3][2], wmtx[3][3])) writepipebuf(struct.pack('fff', obj.data.color[0], obj.data.color[1], obj.data.color[2])) type = 2 spotCutoff = 0.0 hasFalloff = False castShadow = False if obj.data.type == 'POINT': type = 2 hasFalloff = True castShadow = obj.data.use_shadow elif obj.data.type == 'SPOT': type = 3 hasFalloff = True spotCutoff = obj.data.spot_size castShadow = obj.data.use_shadow elif obj.data.type == 'SUN': type = 1 castShadow = obj.data.use_shadow constant = 1.0 linear = 0.0 quadratic = 0.0 if hasFalloff: if obj.data.falloff_type == 'INVERSE_COEFFICIENTS': constant = obj.data.constant_coefficient linear = obj.data.linear_coefficient quadratic = obj.data.quadratic_coefficient layer = 0 if 'retro_layer' in obj.data.keys(): layer = obj.data['retro_layer'] writepipebuf(struct.pack('IIfffffb', layer, type, obj.data.energy, spotCutoff, constant, linear, quadratic, castShadow)) writepipestr(obj.name.encode()) # Command loop for reading data from blender def dataout_loop(): writepipestr(b'READY') while True: cmdargs = read_cmdargs() print(cmdargs) if cmdargs[0] == 'DATAEND': writepipestr(b'DONE') return elif cmdargs[0] == 'MESHLIST': meshCount = 0 for meshobj in bpy.data.objects: if meshobj.type == 'MESH' and not meshobj.data.library: meshCount += 1 writepipebuf(struct.pack('I', meshCount)) for meshobj in bpy.data.objects: if meshobj.type == 'MESH' and not meshobj.data.library: writepipestr(meshobj.name.encode()) elif cmdargs[0] == 'LIGHTLIST': lightCount = 0 for obj in bpy.context.scene.objects: if obj.type == 'LIGHT' and not obj.data.library: lightCount += 1 writepipebuf(struct.pack('I', lightCount)) for obj in bpy.context.scene.objects: if obj.type == 'LIGHT' and not obj.data.library: writepipestr(obj.name.encode()) elif cmdargs[0] == 'MESHAABB': writepipestr(b'OK') hecl.mesh_aabb(writepipebuf) elif cmdargs[0] == 'MESHCOMPILE': meshName = bpy.context.scene.hecl_mesh_obj if meshName not in bpy.data.objects: writepipestr(('mesh %s not found' % meshName).encode()) continue writepipestr(b'OK') hecl.hmdl.cook(writepipebuf, bpy.data.objects[meshName]) elif cmdargs[0] == 'ARMATURECOMPILE': armName = bpy.context.scene.hecl_arm_obj if armName not in bpy.data.objects: writepipestr(('armature %s not found' % armName).encode()) continue writepipestr(b'OK') hecl.armature.cook(writepipebuf, bpy.data.objects[armName].data) elif cmdargs[0] == 'MESHCOMPILENAME': meshName = cmdargs[1] useLuv = int(cmdargs[2]) if meshName not in bpy.data.objects: writepipestr(('mesh %s not found' % meshName).encode()) continue writepipestr(b'OK') hecl.hmdl.cook(writepipebuf, bpy.data.objects[meshName], useLuv) elif cmdargs[0] == 'MESHCOMPILENAMECOLLISION': meshName = cmdargs[1] if meshName not in bpy.data.objects: writepipestr(('mesh %s not found' % meshName).encode()) continue writepipestr(b'OK') hecl.hmdl.cookcol(writepipebuf, bpy.data.objects[meshName]) elif cmdargs[0] == 'MESHCOMPILECOLLISIONALL': writepipestr(b'OK') colCount = 0 for obj in bpy.context.scene.objects: if obj.type == 'MESH' and not obj.data.library: colCount += 1 writepipebuf(struct.pack('I', colCount)) for obj in bpy.context.scene.objects: if obj.type == 'MESH' and not obj.data.library: hecl.hmdl.cookcol(writepipebuf, obj) elif cmdargs[0] == 'MESHCOMPILEPATH': meshName = bpy.context.scene.hecl_path_obj if meshName not in bpy.data.objects: writepipestr(('mesh %s not found' % meshName).encode()) continue writepipestr(b'OK') hecl.path.cook(writepipebuf, bpy.data.objects[meshName]) elif cmdargs[0] == 'WORLDCOMPILE': writepipestr(b'OK') hecl.swld.cook(writepipebuf) elif cmdargs[0] == 'FRAMECOMPILE': version = int(cmdargs[1]) if version != 0 and version != 1: writepipestr(b'bad version') continue writepipestr(b'OK') buffer = hecl.frme.cook(writepipebuf, version, PathHasher()) writepipestr(b'FRAMEDONE') writepipebuf(struct.pack('I', len(buffer))) writepipebuf(buffer) elif cmdargs[0] == 'LIGHTCOMPILEALL': writepipestr(b'OK') lampCount = 0 firstSpot = None for obj in bpy.context.scene.objects: if obj.type == 'LIGHT': lampCount += 1 if firstSpot is None and obj.data.type == 'SPOT': firstSpot = obj # Ambient world = bpy.context.scene.world ambient_energy = 0.0 ambient_color = None if world.use_nodes and 'Background' in world.node_tree.nodes: bg_node = world.node_tree.nodes['Background'] ambient_energy = bg_node.inputs[1].default_value ambient_color = bg_node.inputs[0].default_value if ambient_energy: lampCount += 1 writepipebuf(struct.pack('I', lampCount)) if firstSpot is not None: writelight(firstSpot) if ambient_energy: writepipebuf(struct.pack('ffffffffffffffff', 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)) writepipebuf(struct.pack('fff', ambient_color[0], ambient_color[1], ambient_color[2])) writepipebuf(struct.pack('IIfffffb', 0, 0, ambient_energy, 0.0, 1.0, 0.0, 0.0, False)) writepipestr(b'AMBIENT') # Lamp objects for obj in bpy.context.scene.objects: if obj != firstSpot and obj.type == 'LIGHT': writelight(obj) elif cmdargs[0] == 'GETTEXTURES': writepipestr(b'OK') img_count = 0 for img in bpy.data.images: if img.type == 'IMAGE': img_count += 1 writepipebuf(struct.pack('I', img_count)) for img in bpy.data.images: if img.type == 'IMAGE': path = os.path.normpath(bpy.path.abspath(img.filepath)) writepipebuf(struct.pack('I', len(path))) writepipebuf(path.encode()) elif cmdargs[0] == 'ACTORCOMPILE': writepipestr(b'OK') hecl.sact.cook(writepipebuf) elif cmdargs[0] == 'ACTORCOMPILECHARACTERONLY': writepipestr(b'OK') hecl.sact.cook_character_only(writepipebuf) elif cmdargs[0] == 'ACTIONCOMPILECHANNELSONLY': actionName = cmdargs[1] writepipestr(b'OK') hecl.sact.cook_action_channels_only(writepipebuf, actionName) elif cmdargs[0] == 'GETSUBTYPENAMES': writepipestr(b'OK') hecl.sact.get_subtype_names(writepipebuf) elif cmdargs[0] == 'GETSUBTYPEOVERLAYNAMES': subtypeName = cmdargs[1] writepipestr(b'OK') hecl.sact.get_subtype_overlay_names(writepipebuf, subtypeName) elif cmdargs[0] == 'GETATTACHMENTNAMES': writepipestr(b'OK') hecl.sact.get_attachment_names(writepipebuf) elif cmdargs[0] == 'GETACTIONNAMES': writepipestr(b'OK') hecl.sact.get_action_names(writepipebuf) elif cmdargs[0] == 'GETBONEMATRICES': armName = cmdargs[1] if armName not in bpy.data.objects: writepipestr(('armature %s not found' % armName).encode()) continue armObj = bpy.data.objects[armName] if armObj.type != 'ARMATURE': writepipestr(('object %s not an ARMATURE' % armName).encode()) continue writepipestr(b'OK') writepipebuf(struct.pack('I', len(armObj.data.bones))) for bone in armObj.data.bones: writepipebuf(struct.pack('I', len(bone.name))) writepipebuf(bone.name.encode()) for r in bone.matrix_local.to_3x3(): for c in r: writepipebuf(struct.pack('f', c)) elif cmdargs[0] == 'RENDERPVS': pathOut = cmdargs[1] locX = float(cmdargs[2]) locY = float(cmdargs[3]) locZ = float(cmdargs[4]) hecl.srea.render_pvs(pathOut, (locX, locY, locZ)) writepipestr(b'OK') elif cmdargs[0] == 'RENDERPVSLIGHT': pathOut = cmdargs[1] lightName = cmdargs[2] hecl.srea.render_pvs_light(pathOut, lightName) writepipestr(b'OK') elif cmdargs[0] == 'MAPAREACOMPILE': if 'MAP' not in bpy.data.objects: writepipestr(('"MAP" object not in .blend').encode()) continue map_obj = bpy.data.objects['MAP'] if map_obj.type != 'MESH': writepipestr(('object "MAP" not a MESH').encode()) continue writepipestr(b'OK') hecl.mapa.cook(writepipebuf, map_obj) elif cmdargs[0] == 'MAPUNIVERSECOMPILE': writepipestr(b'OK') hecl.mapu.cook(writepipebuf) loaded_blend = None # Main exception handling try: # Command loop while True: cmdargs = read_cmdargs() print(cmdargs) if cmdargs[0] == 'QUIT': quitblender() elif cmdargs[0] == 'OPEN': if 'FINISHED' in bpy.ops.wm.open_mainfile(filepath=cmdargs[1]): if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT') loaded_blend = cmdargs[1] writepipestr(b'FINISHED') else: writepipestr(b'CANCELLED') elif cmdargs[0] == 'CREATE': if len(cmdargs) >= 4: bpy.ops.wm.open_mainfile(filepath=cmdargs[3]) else: bpy.ops.wm.read_homefile(use_empty=True) bpy.context.scene.world = bpy.data.worlds.new('World') loaded_blend = cmdargs[1] bpy.context.preferences.filepaths.save_version = 0 if 'FINISHED' in bpy.ops.wm.save_as_mainfile(filepath=cmdargs[1]): bpy.ops.file.hecl_patching_load() bpy.context.scene.hecl_type = cmdargs[2] writepipestr(b'FINISHED') else: writepipestr(b'CANCELLED') elif cmdargs[0] == 'GETTYPE': writepipestr(bpy.context.scene.hecl_type.encode()) elif cmdargs[0] == 'GETMESHRIGGED': meshName = bpy.context.scene.hecl_mesh_obj if meshName not in bpy.data.objects: writepipestr(b'FALSE') else: if len(bpy.data.objects[meshName].vertex_groups): writepipestr(b'TRUE') else: writepipestr(b'FALSE') elif cmdargs[0] == 'SAVE': bpy.context.preferences.filepaths.save_version = 0 print('SAVING %s' % loaded_blend) if loaded_blend: if 'FINISHED' in bpy.ops.wm.save_as_mainfile(filepath=loaded_blend, check_existing=False, compress=True): writepipestr(b'FINISHED') else: writepipestr(b'CANCELLED') elif cmdargs[0] == 'PYBEGIN': writepipestr(b'READY') globals = {'hecl':hecl} compbuf = str() bracket_count = 0 while True: try: line = readpipestr() # ANIM check if line == b'PYANIM': # Ensure remaining block gets executed if len(compbuf): exec_compbuf(compbuf, globals) compbuf = str() animin_loop(globals) continue # End check elif line == b'PYEND': # Ensure remaining block gets executed if len(compbuf): exec_compbuf(compbuf, globals) compbuf = str() writepipestr(b'DONE') break # Syntax filter linestr = line.decode().rstrip() if not len(linestr) or linestr.lstrip()[0] == '#': writepipestr(b'OK') continue leading_spaces = len(linestr) - len(linestr.lstrip()) # Block lines always get appended right away if linestr.endswith(':') or leading_spaces or bracket_count: if len(compbuf): compbuf += '\n' compbuf += linestr bracket_count += count_brackets(linestr) writepipestr(b'OK') continue # Complete non-block statement in compbuf if len(compbuf): exec_compbuf(compbuf, globals) # Establish new compbuf compbuf = linestr bracket_count += count_brackets(linestr) except Exception as e: writepipestr(b'EXCEPTION') raise break writepipestr(b'OK') elif cmdargs[0] == 'PYEND': writepipestr(b'ERROR') elif cmdargs[0] == 'DATABEGIN': try: dataout_loop() except Exception as e: writepipestr(b'EXCEPTION') raise elif cmdargs[0] == 'DATAEND': writepipestr(b'ERROR') else: hecl.command(cmdargs, writepipestr, writepipebuf) except Exception: fout = open(err_path, 'w') traceback.print_exc(file=fout) fout.close() raise