def print_header(self): core.MY_PRINT_FUNC(core.PACKAGE_VERSION) core.MY_PRINT_FUNC( "Begin by selecting a script above, then click 'Run'") core.MY_PRINT_FUNC( "Click 'Help' to print out details of what the selected script does" ) return
def change_mode(self, *args): # need to have *args here even if i dont use them # the the currently displayed item in the dropdown menu newstr = self.optionvar.get() # find which index within all_script_list it corresponds to idx = [x[0] for x in self.all_script_list].index(newstr) # set helptext and execute func self.helptext = self.all_script_list[idx][1] self.payload = self.all_script_list[idx][2] core.MY_PRINT_FUNC(">>>>>>>>>>") core.MY_PRINT_FUNC("Load new script '%s'" % newstr) core.MY_PRINT_FUNC("") return
def main(): m = core.prompt_user_filename(".pmx") pmx = pmxlib.read_pmx(m) core.MY_PRINT_FUNC("") # valid input is any string that can matched aginst a morph idx s = core.MY_GENERAL_INPUT_FUNC( lambda x: morph_scale.get_idx_in_pmxsublist(x, pmx.morphs) is not None, [ "Please specify the target morph: morph #, JP name, or EN name (names are not case sensitive).", "Empty input will quit the script." ]) # do it again, cuz the lambda only returns true/false morph = morph_scale.get_idx_in_pmxsublist(s, pmx.morphs) print(pmx.morphs[morph].name_jp) newmorphitems = [] print("target morph controls %d verts" % len(pmx.morphs[morph].items)) count = 0 for item in pmx.morphs[morph].items: item: pmxstruct.PmxMorphItemVertex v = pmx.verts[item.vert_idx] wtype = v.weighttype w = v.weight # already know its all mode1 rot = 0 # only care about BDEF2, right? or sdef # if not a bdef2 vertex, then rot=0 meaning no change if wtype == 1 or wtype == 3: for b, r in zip(matchbones, rotamt): # get the weight %, multiply it by how much the bone is rotated by if w[0] == b: rot += r * w[2] elif w[1] == b: rot += r * (1 - w[2]) # count how many actually get rotated if rot != 0: count += 1 # convert from degrees to radians for rotate2d() rot = math.radians(rot) # now the YZ component of the morph vector is rotated around the origin ny, nz = core.rotate2d((0, 0), rot, item.move[1:3]) newitem = pmxstruct.PmxMorphItemVertex(item.vert_idx, [item.move[0], ny, nz]) newmorphitems.append(newitem) print("partial-rotated %d verts" % count) newmorph = pmxstruct.PmxMorph("v-rot", "v-rot", 1, 1, newmorphitems) pmx.morphs.append(newmorph) # done iter, now write OUT = core.get_unused_file_name("NEW.pmx") pmxlib.write_pmx(OUT, pmx) print("done")
def do_the_thing(self): core.MY_PRINT_FUNC("="*50) core.MY_PRINT_FUNC(str(self.optionvar.get())) # disable all gui elements for the duration of this function # run_butt, spinbox, clear, help, debug self.run_butt.configure(state='disabled') self.which_script.configure(state='disabled') self.clear_butt.configure(state='disabled') self.help_butt.configure(state='disabled') self.debug_check.configure(state='disabled') try: self.payload(bool(self.debug_check_var.get())) except Exception as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC("ERROR: failed to complete target script") # re-enable GUI elements when finished running self.run_butt.configure(state='normal') self.which_script.configure(state='normal') self.clear_butt.configure(state='normal') self.help_butt.configure(state='normal') self.debug_check.configure(state='normal') return
def gui_fileprompt(extensions: str) -> str: """ Use a Tkinter File Dialogue popup to prompt for a file. Same signature as core.prompt_user_filename(). :param extensions: string of valid extensions, separated by spaces :return: case-correct absolute file path """ # replaces core func MY_FILEPROMPT_FUNC when running in GUI mode # make this list into a new, separate thing: list of identifiers + globs if extensions in FILE_EXTENSION_MAP: extensions_labels = FILE_EXTENSION_MAP[extensions] else: extensions_labels = ("Unknown type", extensions) extensions_labels = (extensions_labels, ) # dont trust file dialog to remember last-opened path, manually save/read it recordpath = core.get_persistient_storage_path("last_opened_dir.txt") c = core.read_txtfile_to_list(recordpath, quiet=True) if c: # if it has been used before, use the path from last time. c = c[0] # if the path from last time does not exist, walk up the path till I find a level that does still exist. while c and not path.isdir(c): c = path.dirname(c) start_here = c else: # if never used before, start in the executable directory start_here = "." newpath = fdg.askopenfilename(initialdir=start_here, title="Select input file: {%s}" % extensions, filetypes=extensions_labels) # if user closed the prompt before giving a file path, quit here if newpath == "": core.MY_PRINT_FUNC("ERROR: this script requires an input file to run") raise RuntimeError() # they got an existing file! update the last_opened_dir file core.write_list_to_txtfile(recordpath, [path.dirname(newpath)], quiet=True) return newpath
def gui_inputpopup_trigger(args, explain_info=None): # print("trig") global inputpopup_args # write into simplechoice_args to signify that I want a popup inputpopup_args = [args, explain_info] # wait for a choice to be made from within the popup inputpopup_done.wait() inputpopup_done.clear() # if they clicked x ... if inputpopup_result is None: if callable(args): # this is general-input mode # return empty string (usually aborts the script) return "" else: # this is simplechoice (multichoice) mode # return the first option return args[0] else: core.MY_PRINT_FUNC(str(inputpopup_result)) return inputpopup_result
def print_header(self): core.MY_PRINT_FUNC("Nuthouse01 - 08/24/2020 - v5.00") core.MY_PRINT_FUNC("Begin by selecting a script above, then click 'Run'") core.MY_PRINT_FUNC("Click 'Help' to print out details of what the selected script does") return
def help_func(self): core.MY_PRINT_FUNC(self.helptext)
def gui_inputpopup(args, explain_info=None): # print("pop") # create popup win = tk.Toplevel() win.title("User input needed") # normally when X button is pressed, it calls "destroy". that would leave the script-thread indefinitely waiting on the flag! # this redefine will set the flag so the script resumes when X is clicked def on_x(): global inputpopup_result inputpopup_result = None inputpopup_done.set() win.destroy() win.protocol("WM_DELETE_WINDOW", on_x) # init the result to None, just because global inputpopup_result inputpopup_result = None # if explain_info is given, create labels that display those strings if isinstance(explain_info, str): explain_info = [explain_info] if explain_info is not None: labelframe = tk.Frame(win) labelframe.pack(side=tk.TOP, fill='x') for f in explain_info: # create labels for each line label = tk.Label(labelframe, text=f) label.pack(side=tk.TOP, fill='x', padx=10, pady=10) core.MY_PRINT_FUNC(f) # this function commits the result & closes the popup def setresult(r): global inputpopup_result inputpopup_result = r # pressing the button should stop the mainloop inputpopup_done.set() win.destroy() # build a frame for the interactables to live in buttonframe = tk.Frame(win) buttonframe.pack(side=tk.TOP) # guarantee the popup is in front of the main window win.lift() # guarantee the popup has focus win.focus_set() # decide what the mode is & how to fill the popup if callable(args): # this is general-input mode, create text-entry box and submit button # for some reason the snow/white color still looks beige? :( oh well i tried textbox = tk.Entry(buttonframe, width=50, bg='snow') textbox.pack(side=tk.TOP, padx=10, pady=10) def submit_callback(event=None): # validate the text input using the validity check function "args" # if its good then invoke "setresult", if its bad then clear the text box # the func should be defined to print something explaining why it failed whenever it fails t = textbox.get().rstrip() if args(t): setresult(t) else: textbox.delete(0, tk.END) submit = tk.Button(buttonframe, text="Submit", command=submit_callback) submit.pack(side=tk.TOP, padx=10, pady=10) # "enter" key will be equivalent to clicking the submit button... # (technically this will happen whenever focus is inside the popup window, not just when focus is in the text entry box, but oh well) win.bind('<Return>', submit_callback) # guarantee the textbox within the popup has focus so user can start typing immediately (requires overall popup to already have focus) textbox.focus_set() else: # this is simplechoice (multichoice) mode, "args" is a list... create buttons for each option # create buttons for each numbered option for i in args: # each button will call "setresult" with its corresponding number, lambda needs to be written EXACTLY like this, i forget why it works c = lambda v=i: setresult(v) button = tk.Button(buttonframe, text=str(i), command=c) button.pack(side=tk.LEFT, padx=10, pady=10) return None
def main(): print( "Open all PMX files at the selected level and replace usages of texure file XXXXX with YYYYY" ) core.MY_PRINT_FUNC("Please enter name of PMX model file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") # absolute path to directory holding the pmx input_filename_pmx_abs = os.path.normpath( os.path.abspath(input_filename_pmx)) startpath, input_filename_pmx_rel = os.path.split(input_filename_pmx_abs) # ========================================================================================================= # ========================================================================================================= # ========================================================================================================= # first, build the list of ALL files that actually exist, then filter it down to neighbor PMXs relative_all_exist_files = file_sort_textures.walk_filetree_from_root( startpath) # now fill "neighbor_pmx" by finding files without path separator that end in PMX # these are relative paths tho pmx_filenames = [ f for f in relative_all_exist_files if (f.lower().endswith(".pmx")) and (os.path.sep not in f) ] # now read all the PMX objects & store in dict alongside the relative name # dictionary where keys are filename and values are resulting pmx objects all_pmx_obj = {} for this_pmx_name in pmx_filenames: this_pmx_obj = pmxlib.read_pmx(os.path.join(startpath, this_pmx_name), moreinfo=False) all_pmx_obj[this_pmx_name] = this_pmx_obj core.MY_PRINT_FUNC("ALL PMX FILES:") for pmxname in pmx_filenames: core.MY_PRINT_FUNC(" " + pmxname) core.MY_PRINT_FUNC("\n\n\n") core.MY_PRINT_FUNC( "WARNING: this script will overwrite all PMX files it operates on. This does NOT create a backup. Be very careful what you type!" ) core.MY_PRINT_FUNC("\n\n\n") findme = core.MY_GENERAL_INPUT_FUNC( lambda x: True, "Please specify which filepath to find:") findme = os.path.normpath(findme.strip()) # sanitize it # if empty, quit if findme == "" or findme is None: core.MY_PRINT_FUNC("quitting") return None replacewith = core.MY_GENERAL_INPUT_FUNC( lambda x: True, "Please specify which filepath to replace it with:") replacewith = os.path.normpath(replacewith.strip()) # sanitize it # if empty, quit if replacewith == "" or replacewith is None: core.MY_PRINT_FUNC("quitting") return None core.MY_PRINT_FUNC("Replacing '%s' with '%s'" % (findme, replacewith)) # now do find & replace! # for each pmx, for this_pmx_name, this_pmx_obj in all_pmx_obj.items(): # do find-and-replace howmany = file_sort_textures.texname_find_and_replace(this_pmx_obj, findme, replacewith, sanitize=True) # then report how many core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("'%s': replaced %d" % (this_pmx_name, howmany)) if howmany != 0: # NOTE: this is OVERWRITING THE PREVIOUS PMX FILE, NOT CREATING A NEW ONE # because I make a zipfile backup I don't need to feel worried about preserving the old version output_filename_pmx = os.path.join(startpath, this_pmx_name) # output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, this_pmx_obj, moreinfo=False) core.MY_PRINT_FUNC("Done!") return None
def main(moreinfo=True): # prompt PMX name core.MY_PRINT_FUNC("Please enter name of PMX input file:") input_filename_pmx = core.MY_FILEPROMPT_FUNC(".pmx") pmx = pmxlib.read_pmx(input_filename_pmx, moreinfo=moreinfo) # coordinates are stored as list[x, y, z], convert this --> tuple --> hash for much faster comparing vert_coord_hashes = [hash(tuple(v.pos)) for v in pmx.verts] all_vert_sets = [] all_face_sets = [] all_bone_indices = [] all_rigidbody_indices = [] start_vert = 0 start_face = 0 # i know i'm done when i have consumed all verts & all faces while start_face < len(pmx.faces) and start_vert < len(pmx.verts): # 1. start a new sets for the vertices and faces vert_set = set() face_set = set() # 2. pick a vertex that hasn't been used yet and add it to the set, ez # 2b. optimization: a fragment is guaranteed to have at least 4 faces (to make a closed 3d solid) and therefore at least 4 verts # can i safely assume that they are "sharp" corners and therefore there are 12 verts? for i in range(12): vert_set.add(start_vert + i) # also, init the faces set with the minimum of 4 faces, and add any verts included in those faces to the vert set for i in range(4): face_set.add(start_face + i) for v in pmx.faces[start_face + i]: # for each vert in this face, vert_set.add(v) # add this vert to the vert set # guarantee that it is contiguous from start_vert to the highest index that was in the faces vert_set = set(list(range(start_vert, max(vert_set) + 1))) # now i have initialized the set with everything i know is guarnateed part of the fragment highest_known_vert = max(vert_set) highest_known_face = max(face_set) # print(str(len(vert_set)) + " ", end="") # begin looping & flooding until i don't detect any more while True: # 3. note the number of verts collected so far set_size_A = len(vert_set) # 4. find all faces that include any vertex in the "fragment set", # whenever i find one, add all verts that it includes to the "fragment set" as well ''' # zero-assumption brute-force method: for f_id in range(len(pmx.faces)): face = pmx.faces[f_id] if face[0] in vert_set or face[1] in vert_set or face[2] in vert_set: # we got a hit! face_set.add(f_id) vert_set.add(face[0]) vert_set.add(face[1]) vert_set.add(face[2]) ''' # optimization: scan only faces index 'highest_known_face+1' thru 'highest_known_face'+LOOKAHEAD # because 0 thru start_face is guaranteed to not be part of the group # and start_face thru highest_known_face is already guaranteed to be part of the group # if chunks are bigger than LOOKAHEAD, then it's not guaranteed to succeed or fail, could do either for f_id in range( highest_known_face + 1, min(highest_known_face + LOOKAHEAD, len(pmx.faces))): face = pmx.faces[f_id] if face[0] in vert_set or face[1] in vert_set or face[ 2] in vert_set: # we got a hit! face_set.add(f_id) vert_set.add(face[0]) vert_set.add(face[1]) vert_set.add(face[2]) # optimization: if this is farther than what i thought was the end, then everything before it should be added too if f_id > highest_known_face: for x in range(highest_known_face + 1, f_id): face_set.add(x) vert_set.add(pmx.faces[x][0]) vert_set.add(pmx.faces[x][1]) vert_set.add(pmx.faces[x][2]) highest_known_face = f_id set_size_B = len(vert_set) # update the set of vertex coord hashes for easier comparing vert_set_hashes = set([vert_coord_hashes[i] for i in vert_set]) # 5. find all vertices that have the same exact coordinates as any vertex in the "fragment set", # then and add them to the "fragment set" ''' # zero-assumption brute-force method: for v_id in range(len(vert_coord_hashes)): vert_hash = vert_coord_hashes[v_id] if vert_hash in vert_set_hashes: # we got a hit! vert_set.add(v_id) ''' # optimization: scan only verts index 'highest_known_vert+1' thru 'highest_known_vert'+LOOKAHEAD # because 0 thru start_vert is guaranteed to not be part of the group # and start_vert thru highest_known_vert is already guaranteed to be part of the group # if chunks are bigger than LOOKAHEAD, then it's not guaranteed to succeed or fail, could do either for v_id in range( highest_known_vert + 1, min(highest_known_vert + LOOKAHEAD, len(pmx.verts))): vert_hash = vert_coord_hashes[v_id] if vert_hash in vert_set_hashes: # we got a hit! vert_set.add(v_id) # optimization: if this is farther than what i thought was the end, then everything before it should be added too if v_id > highest_known_vert: for x in range(highest_known_vert + 1, v_id): vert_set.add(x) highest_known_vert = v_id set_size_C = len(vert_set) print("+%d +%d, " % (set_size_B - set_size_A, set_size_C - set_size_B), end="") # 6. if the number of verts did not change, we are done if set_size_C == set_size_A: break pass print("") # 7. now i have a complete fragment in vert_set and face_set !! :) all_vert_sets.append(vert_set) all_face_sets.append(face_set) # increment the face-start and vert-start indices, this is my stop condition start_vert += len(vert_set) start_face += len(face_set) # move on to the next fragment if i still have more verts to parse pass # done with identifying all fragments! # double-check that all vertices got sorted into one and only one fragment assert sum([len(vs) for vs in all_vert_sets]) == len(pmx.verts) temp = set() for vs in all_vert_sets: temp.update(vs) assert len(temp) == len(pmx.verts) # double-check that all faces got sorted into one and only one fragment assert sum([len(fs) for fs in all_face_sets]) == len(pmx.faces) temp = set() for fs in all_face_sets: temp.update(fs) assert len(temp) == len(pmx.faces) print("") print("Identified %d discrete fragments!" % (len(all_vert_sets), )) # BONES AND WEIGHTS for fragnum in range(len(all_vert_sets)): # name newbone_name = "fragment%d" % fragnum # position: average of all vertices in the fragment? sure why not # TODO is there a "better" way of calculating the average/centroid/center of mass? idk newbone_pos = [0, 0, 0] for v_id in all_vert_sets[fragnum]: # accumulate the XYZ for each vertex in the fragment newbone_pos[0] += pmx.verts[v_id].pos[0] newbone_pos[1] += pmx.verts[v_id].pos[1] newbone_pos[2] += pmx.verts[v_id].pos[2] # divide by the number of verts in the fragment to get the average newbone_pos[0] /= len(all_vert_sets[fragnum]) newbone_pos[1] /= len(all_vert_sets[fragnum]) newbone_pos[2] /= len(all_vert_sets[fragnum]) # create the new bone object newbone_obj = pmxstruct.PmxBone( name_jp=newbone_name, name_en=newbone_name, pos=newbone_pos, parent_idx=0, deform_layer=0, deform_after_phys=False, has_rotate=True, has_translate=True, has_visible=True, has_enabled=True, has_ik=False, tail_usebonelink=False, tail=[0, 0, 0], inherit_rot=False, inherit_trans=False, has_fixedaxis=False, has_localaxis=False, has_externalparent=False, ) # note the index it will be inserted at thisboneindex = len(pmx.bones) all_bone_indices.append(thisboneindex) # append it onto the list of bones pmx.bones.append(newbone_obj) # for each vertex in this fragment, give it 100% weight on that bone for v_id in all_vert_sets[fragnum]: v = pmx.verts[v_id] v.weighttype = pmxstruct.WeightMode.BDEF1 # BDEF1 v.weight = [[thisboneindex, 1]] pass # RIGID BODIES for fragnum in range(len(all_vert_sets)): newbody_name = "body%d-0" % fragnum newbody_pos = pmx.bones[all_bone_indices[fragnum]].pos # hmmm, what do do here? this is the really hard part! # let's just make a sphere with radius equal to the distance to the nearest vertex of this fragment? # TODO: the bodies created from this are intersecting eachother when at rest! # the distance to the closest vertex is greater than the distance to the closest point on the closest face! # therefore there is a small bit of overlap newbody_radius = dist_to_nearest_vertex(newbody_pos, all_vert_sets[fragnum], pmx) # TODO: to "fill a fragment with several rigidbody spheres", you need to a) select a center for each, b) select a size for each # the sizes can come from algorithm roughed out in dist_to_nearest_point_on_mesh_surface() # the centers... idk? how can you do this? # https://doc.babylonjs.com/toolsAndResources/utilities/InnerMeshPoints might be able to reuse some of the ideas from this? # phys params: set mass equal to the VOLUME of this new rigid body! oh that seems clever, i like that, bigger ones are heavier # if i figure out how to create multiple bodies, each body's mass should be proportional to its volume like this volume = 3.14 * (4 / 3) * (newbody_radius**3) mass = volume * MASS_FACTOR # phys params: use the default damping/friction/etc parameters cuz idk why not phys_move_damp = 0.95 phys_rot_damp = 0.95 phys_friction = 0.95 phys_repel = 0.3 # bounciness? # this gif is with these params: https://gyazo.com/3d143f33b79c1151c1ccbffcc578448b # groups: for now, since each fragment is only one body, i can just ignore groups stuff # groups: later, if each fragment is several bodies... assign the groups in round-robin? each fragment will clip thru 1/15 of the # other fragments but i think that's unavoidable. also need to reserve group16 for the floor! so set each fragment's cluster of # bodies to nocollide with the group# assigned to that cluster, but respect all others. # bone_idx: if there are more than 1 rigidbodies associated with each fragment, one "main" body is connected to the bone # all the others are set to bone -1 and connected to the mainbody via joints newbody_obj = pmxstruct.PmxRigidBody( name_jp=newbody_name, name_en=newbody_name, bone_idx=all_bone_indices[fragnum], pos=newbody_pos, rot=[0, 0, 0], size=[newbody_radius, 0, 0], shape=pmxstruct.RigidBodyShape.SPHERE, group=1, nocollide_set=set(), phys_mode=pmxstruct.RigidBodyPhysMode.PHYSICS, phys_mass=mass, phys_move_damp=phys_move_damp, phys_rot_damp=phys_rot_damp, phys_repel=phys_repel, phys_friction=phys_friction) # note the index that this will be inserted at bodyindex = len(pmx.rigidbodies) all_rigidbody_indices.append(bodyindex) pmx.rigidbodies.append(newbody_obj) pass # JOINTS # if there is only one body per fragment then this is okay without any joints # if there are several bodies then we need to create joints from the "center" rigidbody to the others # even if you try to limit the joint to 0 rotation and 0 slide it still has some wiggle in it :( not perfectly rigid # TODO: i'll deal with this if and only if an algorithm for filling fragments with rigidbodies is created for fragnum in range(len(all_vert_sets)): pass core.MY_PRINT_FUNC("") # write out output_filename_pmx = input_filename_pmx[0:-4] + "_fragfix.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None
all_rigidbody_indices.append(bodyindex) pmx.rigidbodies.append(newbody_obj) pass # JOINTS # if there is only one body per fragment then this is okay without any joints # if there are several bodies then we need to create joints from the "center" rigidbody to the others # even if you try to limit the joint to 0 rotation and 0 slide it still has some wiggle in it :( not perfectly rigid # TODO: i'll deal with this if and only if an algorithm for filling fragments with rigidbodies is created for fragnum in range(len(all_vert_sets)): pass core.MY_PRINT_FUNC("") # write out output_filename_pmx = input_filename_pmx[0:-4] + "_fragfix.pmx" output_filename_pmx = core.get_unused_file_name(output_filename_pmx) pmxlib.write_pmx(output_filename_pmx, pmx, moreinfo=moreinfo) core.MY_PRINT_FUNC("Done!") return None if __name__ == '__main__': print(_SCRIPT_VERSION) # print info to explain the purpose of this file core.MY_PRINT_FUNC(helptext) core.MY_PRINT_FUNC("") main() core.pause_and_quit("Done with everything! Goodbye!")
def main(moreinfo=False): # 1. user input core.MY_PRINT_FUNC("Current dir = '%s'" % os.getcwd()) core.MY_PRINT_FUNC( "Enter the path to the root folder that contains ALL models:") while True: name = input("root folder = ") if not os.path.isdir(name): core.MY_PRINT_FUNC(os.path.abspath(name)) core.MY_PRINT_FUNC( "Err: given folder does not exist, did you type it wrong?") else: break # it exists, so make it absolute rootdir = os.path.abspath(os.path.normpath(name)) core.MY_PRINT_FUNC("root folder = '%s'" % rootdir) core.MY_PRINT_FUNC("") core.MY_PRINT_FUNC("... beginning to index file tree...") # 2. build list of ALL file on the system within this folder relative_all_exist_files = file_sort_textures.walk_filetree_from_root( rootdir) core.MY_PRINT_FUNC("... total # of files:", len(relative_all_exist_files)) relative_all_pmx = [ f for f in relative_all_exist_files if f.lower().endswith(".pmx") ] core.MY_PRINT_FUNC("... total # of PMX models:", len(relative_all_pmx)) relative_exist_img_files = [ f for f in relative_all_exist_files if f.lower().endswith(IMG_EXT) ] core.MY_PRINT_FUNC("... total # of image sources:", len(relative_exist_img_files)) core.MY_PRINT_FUNC("") # this will accumulate the list of PMXes list_of_pmx_with_missing_tex = [] list_of_pmx_that_somehow_failed = [] # 3. for each pmx, for d, pmx_name in enumerate(relative_all_pmx): # progress print core.MY_PRINT_FUNC("\n%d / %d" % (d + 1, len(relative_all_pmx))) # wrap the actual work with a try-catch just in case # this is a gigantic time investment and I dont want it to fail halfway thru and lose everything try: # 4. read the pmx, gotta store it in the dict like this cuz shut up thats why # dictionary where keys are filename and values are resulting pmx objects all_pmx_obj = {} this_pmx_obj = pmxlib.read_pmx(os.path.join(rootdir, pmx_name), moreinfo=False) all_pmx_obj[pmx_name] = this_pmx_obj # 5. filter images down to only images underneath the same folder as the pmx pmx_folder = os.path.dirname(pmx_name).lower() possible_img_sources = [ f for f in relative_exist_img_files if f.lower().startswith(pmx_folder) ] # trim the leading "pmx_folder" portion from these names possible_img_sources = [ os.path.relpath(f, pmx_folder) for f in possible_img_sources ] # 6. make filerecord_list # for each pmx, for each file on disk, match against files used in textures (case-insensitive) and replace with canonical name-on-disk # also fill out how much and how each file is used, and unify dupes between files, all that good stuff filerecord_list = file_sort_textures.build_filerecord_list( all_pmx_obj, possible_img_sources, False) # 7. if within filerecordlist, any filerecord is used but does not exist, if any(((fr.numused != 0) and (not fr.exists)) for fr in filerecord_list): # then save this pmx name list_of_pmx_with_missing_tex.append(pmx_name) except Exception as e: core.MY_PRINT_FUNC(e.__class__.__name__, e) core.MY_PRINT_FUNC( "ERROR! some kind of exception interrupted reading pmx '%s'" % pmx_name) list_of_pmx_that_somehow_failed.append(pmx_name) core.MY_PRINT_FUNC("\n\n") # make the paths absolute list_of_pmx_that_somehow_failed = [ os.path.join(rootdir, p) for p in list_of_pmx_that_somehow_failed ] list_of_pmx_with_missing_tex = [ os.path.join(rootdir, p) for p in list_of_pmx_with_missing_tex ] # print & write-to-file if list_of_pmx_that_somehow_failed: core.MY_PRINT_FUNC("WARNING: failed in some way on %d PMX files" % len(list_of_pmx_that_somehow_failed)) core.MY_PRINT_FUNC("Writing the full list to text file:") output_filename_failures = core.get_unused_file_name(FAILED_LIST_FILE) core.write_list_to_txtfile(output_filename_failures, list_of_pmx_that_somehow_failed) core.MY_PRINT_FUNC( "Found %d / %d PMX files that are missing at least one texture source" % (len(list_of_pmx_with_missing_tex), len(relative_all_pmx))) core.MY_PRINT_FUNC("Writing the full list to text file:") output_filename_missingtex = core.get_unused_file_name( MISSINGTEX_LIST_FILE) core.write_list_to_txtfile(output_filename_missingtex, list_of_pmx_with_missing_tex) # print(list_of_pmx_with_missing_tex) core.MY_PRINT_FUNC("Done!") return None