class MainPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) self.bdm = None self.parent = parent self.entry_list = TreeListCtrl(self, style=TL_MULTIPLE) self.entry_list.AppendColumn("Entry") self.entry_list.Bind(EVT_TREELIST_ITEM_CONTEXT_MENU, self.on_right_click) self.entry_list.Bind(EVT_TREELIST_SELECTION_CHANGED, self.on_select) self.cdo = wx.CustomDataObject("BDMEntry") self.Bind(wx.EVT_MENU, self.on_delete, id=wx.ID_DELETE) self.Bind(wx.EVT_MENU, self.on_copy, id=wx.ID_COPY) self.Bind(wx.EVT_MENU, self.on_paste, id=wx.ID_PASTE) self.Bind(wx.EVT_MENU, self.on_new, id=wx.ID_NEW) accelerator_table = wx.AcceleratorTable([ (wx.ACCEL_CTRL, ord('c'), wx.ID_COPY), (wx.ACCEL_CTRL, ord('v'), wx.ID_PASTE), (wx.ACCEL_NORMAL, wx.WXK_DELETE, wx.ID_DELETE), ]) self.entry_list.SetAcceleratorTable(accelerator_table) pub.subscribe(self.on_select, 'on_select') pub.subscribe(self.convert_for_skill_creator, 'convert_for_skill_creator') # Use some sizers to see layout options sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.entry_list, 1, wx.ALL | wx.EXPAND, 10) # Layout sizers self.SetSizer(sizer) self.SetAutoLayout(1) def build_tree(self): self.entry_list.DeleteAllItems() root = self.entry_list.GetRootItem() for entry in sorted(self.bdm.entries, key=lambda e: e.id): self.entry_list.AppendItem(root, f'{entry.id}: Entry', data=entry) def get_current_entry_ids(self): entry_ids = [] item = self.entry_list.GetFirstItem() while item.IsOk(): data = self.entry_list.GetItemData(item) entry_ids.append(data.id) item = self.entry_list.GetNextItem(item) return entry_ids def get_previous_entry(self, entry_id): item = self.entry_list.GetFirstItem() prev = TLI_FIRST while item.IsOk(): data = self.entry_list.GetItemData(item) if data.id > entry_id: break prev, item = item, self.entry_list.GetNextItem(item) return prev def on_right_click(self, _): selected = self.entry_list.GetSelections() if not selected: return menu = wx.Menu() menu.Append(wx.ID_NEW) menu.Append(wx.ID_DELETE) menu.Append(wx.ID_COPY) paste = menu.Append(wx.ID_PASTE) add = menu.Append(wx.ID_ADD, '&Add Copied Entry') success = False # Check Clipboard if wx.TheClipboard.Open(): success = wx.TheClipboard.IsSupported(wx.DataFormat("BDMEntry")) wx.TheClipboard.Close() add.Enable(success) paste.Enable(success) self.PopupMenu(menu) menu.Destroy() def on_select(self, _): selected = self.entry_list.GetSelections() if len(selected) != 1: pub.sendMessage('disable') return pub.sendMessage('load_entry', entry=self.entry_list.GetItemData(selected[0])) def add_entry(self, entry): root = self.entry_list.GetRootItem() prev = self.get_previous_entry(entry.id) item = self.entry_list.InsertItem(root, prev, f'{entry.id}: Entry', data=entry) self.entry_list.Select(item) self.on_select(None) def on_new(self, _): # Ask for ID if self.bdm is None: return with NewEntryDialog(self, self.get_current_entry_ids()) as dlg: if dlg.ShowModal() != wx.ID_OK: return entry_id = dlg.GetValue() self.entry_list.UnselectAll() # Add it entry = Entry(entry_id=entry_id) self.add_entry(entry) pub.sendMessage('set_status_bar', text='Added new entry') def on_add(self, _): if self.bdm is None: return cdo = wx.CustomDataObject("BDMEntry") success = False if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(cdo) wx.TheClipboard.Close() if not success: with wx.MessageDialog(self, 'Unable to get copied data') as dlg: dlg.ShowModal() return paste_data = pickle.loads(cdo.GetData()) # Get new Id's with NewEntryDialog(self, self.get_current_entry_ids()) as dlg: if dlg.ShowModal() != wx.ID_OK: return entry_id = dlg.GetValue() current_entry_ids = self.get_current_entry_ids() self.entry_list.UnselectAll() # Paste for paste in paste_data: while entry_id in current_entry_ids: entry_id += 1 entry = Entry(entry_id=entry_id) entry.paste(paste) self.add_entry(entry) entry_id += 1 self.on_select(None) pub.sendMessage(f'Pasted {len(paste_data)} new entry(s)') def on_delete(self, _): selected = self.entry_list.GetSelections() if not selected: return for item in selected: self.entry_list.DeleteItem(item) pub.sendMessage('disable') pub.sendMessage('set_status_bar', text=f'Deleted {len(selected)} entries') def on_copy(self, _): selected = self.entry_list.GetSelections() self.cdo = wx.CustomDataObject("BDMEntry") self.cdo.SetData( pickle.dumps( [self.entry_list.GetItemData(item) for item in selected])) if wx.TheClipboard.Open(): wx.TheClipboard.SetData(self.cdo) wx.TheClipboard.Flush() wx.TheClipboard.Close() pub.sendMessage('set_status_bar', text=f'Copied {len(selected)} entries') def on_paste(self, _): selected = self.entry_list.GetSelections() if not selected: return success = False cdo = wx.CustomDataObject("BDMEntry") if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(cdo) wx.TheClipboard.Close() if success: paste_data = pickle.loads(cdo.GetData()) paste_length = len(paste_data) selected_length = len(selected) if selected_length > paste_length: for item in selected[paste_length:]: self.entry_list.Unselect(item) selected = selected[:paste_length] item = selected[-1] self.entry_list.Select(item) for n in range(paste_length - selected_length): item = self.entry_list.GetNextItem(item) if not item.IsOk(): with wx.MessageDialog( self, f'Not enough entries to paste over. Expected {paste_length}' ) as dlg: dlg.ShowModal() return self.entry_list.Select(item) selected.append(item) if len(selected) > 1: msg = '\n'.join([ f' * {self.entry_list.GetItemData(item).id}: Entry' for item in selected ]) with MultiMessageDialog( self, 'Are you sure you want to replace the following entries?', 'Warning', msg, wx.YES | wx.NO) as dlg: if dlg.ShowModal() != wx.ID_YES: return for n, paste in enumerate(paste_data): data = self.entry_list.GetItemData(selected[n]) data.paste(paste) self.on_select(None) pub.sendMessage('set_status_bar', text=f'Pasted {len(paste_data)} entry(s)') def convert_for_skill_creator(self): if not self.bdm: with wx.MessageDialog(self, "No BDM loaded!", "Error") as dlg: dlg.ShowModal() return # Get choices choices = set() item = self.entry_list.GetFirstItem() effect_ids = [ 'effect_1_skill_id', 'effect_2_skill_id', 'effect_3_skill_id' ] while item.IsOk(): data = self.entry_list.GetItemData(item) item = self.entry_list.GetNextItem(item) for sub_entry in data.sub_entries: for effect_id in effect_ids: try: if sub_entry[effect_id] != 0 and sub_entry[ effect_id] != 0xFFFF and sub_entry[ effect_id] != 0xBACA: choices.update([str(sub_entry[effect_id])]) except Exception as e: pass if not choices: with wx.MessageDialog(self, "Cannot find any Skill IDs to convert", "Error") as dlg: dlg.ShowModal() return # Show Dialog with ConvertDialog(self, list(choices)) as dlg: if dlg.ShowModal() != wx.ID_OK: return skill_id = dlg.GetValue() # Do Conversion item = self.entry_list.GetFirstItem() changed = 0 while item.IsOk(): data = self.entry_list.GetItemData(item) item = self.entry_list.GetNextItem(item) for sub_entry in data.sub_entries: for effect_id in effect_ids: if sub_entry[effect_id] == skill_id: try: sub_entry[effect_id] = 0xBACA changed += 1 except Exception: pass self.on_select(None) pub.sendMessage('set_status_bar', text=f'Changed {changed} skill ids to 0xBACA') def reindex(self): selected = self.entry_list.GetSelections() if len(selected) != 1: return item = selected[0] entry = self.entry_list.GetItemData(item) self.entry_list.DeleteItem(item) self.add_entry(entry)
class MainPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) self.code = '' self.bac = None self.bdm = None self.ean = None self.cam_ean = None self.dirname = '' self.parent = parent self.links = defaultdict( lambda: defaultdict(lambda: defaultdict(dict))) # Name self.name = wx.StaticText(self, -1, '(No file loaded)') self.font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD) self.name.SetFont(self.font) # Buttons self.open = wx.Button(self, wx.ID_OPEN, "Load") self.save = wx.Button(self, wx.ID_SAVE, "Save") self.save.Disable() self.paste = wx.Button(self, wx.ID_PASTE, "Paste") self.paste.Disable() self.add = wx.Button(self, wx.ID_ADD, "Add Copy") self.add.Disable() # Entry List self.entry_list = TreeListCtrl(self, style=TL_MULTIPLE) self.entry_list.AppendColumn("BAC Entry") self.entry_list.Bind(EVT_TREELIST_ITEM_CONTEXT_MENU, self.on_right_click) # Bind self.Bind(wx.EVT_BUTTON, self.on_open, id=wx.ID_OPEN) self.Bind(wx.EVT_BUTTON, self.on_save, id=wx.ID_SAVE) self.Bind(wx.EVT_BUTTON, self.on_paste, id=wx.ID_PASTE) self.Bind(wx.EVT_BUTTON, self.on_add, id=wx.ID_ADD) self.Bind(wx.EVT_MENU, self.on_paste, id=wx.ID_PASTE) accelerator_table = wx.AcceleratorTable([ (wx.ACCEL_CTRL, ord('v'), wx.ID_PASTE), ]) self.entry_list.SetAcceleratorTable(accelerator_table) self.SetDropTarget(FileDropTarget(self, "load_main_moveset")) # Button Sizer button_sizer = wx.BoxSizer(wx.HORIZONTAL) button_sizer.Add(self.open) button_sizer.AddSpacer(5) button_sizer.Add(self.save) button_sizer.AddSpacer(5) button_sizer.Add(self.paste) button_sizer.AddSpacer(5) button_sizer.Add(self.add) # Use some sizers to see layout options sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.name, 0, wx.CENTER) sizer.Add(button_sizer) sizer.Add(self.entry_list, 1, wx.ALL | wx.EXPAND, 10) # Layout sizers self.SetSizer(sizer) self.SetAutoLayout(1) def on_open(self, _): pub.sendMessage('open_main_moveset') def on_save(self, _): pub.sendMessage('save_moveset') def build_tree(self): self.entry_list.DeleteAllItems() root = self.entry_list.GetRootItem() for i, entry in enumerate(self.bac.entries): if not entry.sub_entries: continue self.entry_list.AppendItem( root, f'{entry.index}: {KNOWN_ENTRIES.get(entry.index, "Unknown")}', data=entry) self.save.Enable() self.paste.Enable() self.add.Enable() def on_right_click(self, _): selected = self.entry_list.GetSelections() if not selected: return menu = wx.Menu() paste = menu.Append(wx.ID_PASTE) paste.Enable(self.parent.copied is not None) self.PopupMenu(menu) menu.Destroy() def find_next_available_index(self, item_type): if item_type == Animation: return len(self.ean.animations) elif item_type == Hitbox: return max([entry.id for entry in self.bdm.entries]) + 1 elif item_type == Camera: return len(self.cam_ean.animations) else: raise (TypeError(f'Unsupported type: {item_type.__name__}')) def find_conflict(self, item_type, entry_pair, depend_value, selected_data, value): if value in list( self.links[item_type][entry_pair][depend_value].values()): return True for entry in self.bac.entries: # Skip this entry if its already in the selected data if entry in selected_data: continue for sub_entry in entry.sub_entries: # Skip if this is not the type we want if item_type != ITEM_TYPES[sub_entry.type]: continue for item in sub_entry.items: if item[entry_pair[1]] == depend_value and item[ entry_pair[0]] == value: return True return False def create_new_index(self, item_type): new_value = self.find_next_available_index(item_type) if item_type == Animation: animation = EanAnimation(self.ean) self.ean.animations.append(animation) elif item_type == Hitbox: self.bdm.entries.append(BdmEntry(entry_id=new_value)) elif item_type == Camera: self.cam_ean.animations.append(EanAnimation(self.cam_ean)) else: raise (TypeError(f'Unsupported type: {item_type.__name__}')) return new_value def file_not_found_dialog(self, filename): with wx.MessageDialog( self, f'{filename} was not opened. Please add it and reload the moveset', 'Error') as dlg: dlg.ShowModal() def invalid_index_dialog(self, filename, index): with wx.MessageDialog(self, f'{filename} does not contain index {index}', 'Error') as dlg: dlg.ShowModal() def copy_index(self, item_type, old_value, new_value): new_code = self.code old_code = self.parent.side_panel.code if item_type == Animation: try: animation = self.ean.animations[new_value] except IndexError: self.invalid_index_dialog(f'{new_code}.ean', new_value) return False try: animation.paste( self.parent.side_panel.ean.animations[old_value], keep_name=True) except IndexError: self.invalid_index_dialog(f'{old_code}.ean', old_value) return False elif item_type == Hitbox: if not self.bdm: self.file_not_found_dialog(f'{new_code}_PLAYER.bdm') return False if not self.parent.side_panel.bdm: self.file_not_found_dialog(f'{old_code}_PLAYER.bdm') return False try: entry = [ entry for entry in self.bdm.entries if entry.id == new_value ][0] except IndexError: self.invalid_index_dialog(f'{new_code}_PLAYER.bdm', new_value) return False try: old_entry = [ entry for entry in self.parent.side_panel.bdm.entries if entry.id == old_value ][0] except IndexError: self.invalid_index_dialog(f'{old_code}_PLAYER.bdm', old_value) return False entry.paste(old_entry) elif item_type == Camera: if not self.cam_ean: self.file_not_found_dialog(f'{new_code}.cam.ean') return False if not self.parent.side_panel.cam_ean: self.file_not_found_dialog(f'{old_code}.cam.ean') return False try: camera = self.cam_ean.animations[new_value] except IndexError: self.invalid_index_dialog(f'{new_code}.cam.ean', new_value) return False try: camera.paste( self.parent.side_panel.cam_ean.animations[old_value], keep_name=True) except IndexError: self.invalid_index_dialog(f'{old_code}.cam.ean', old_value) return False else: raise (TypeError(f'Unsupported type: {item_type.__name__}')) return True def changed_value_message(self, entry_index, changed_values, item_type, old_value, new_value, new=True): old_string = str(old_value) new_string = str(new_value) if item_type == Animation: old_string += f' ({self.parent.side_panel.ean.animations[old_value].name})' new_string += f' ({"*new*" if new else self.ean.animations[new_value].name})' changed_values[item_type].append( (f'[{entry_index}]', new_string, old_string)) def get_changed_values(self, changed_values, item_type, entry_pair, depend_value, entry_values, selected_val, selected_data): entry_index = selected_val[0] selected_val = selected_val[1] entry_name = KNOWN_ENTRIES.get(entry_index, 'Unknown') n = 0 for old_value in entry_values: # Continue if we already have a link old_animation_name = self.parent.side_panel.ean.animations[old_value].name \ if item_type == Animation and old_value < len(self.parent.side_panel.ean.animations) else '' if item_type == Animation and any( word in old_animation_name for word in BLACKLISTED_WORDS) \ and not old_animation_name.endswith(entry_name): continue if old_value in self.links[item_type][entry_pair][depend_value]: new_value = self.links[item_type][entry_pair][depend_value][ old_value] # if (old_value, new_value) in list(zip(*changed_values[item_type])): self.changed_value_message(entry_index, changed_values, item_type, old_value, new_value, False) continue # If the current n is bigger then the selected values or selected values doesn't exist if item_type in selected_val and entry_pair in selected_val[ item_type]: if n >= len(selected_val[item_type][entry_pair][depend_value]): new_value = self.create_new_index(item_type) self.changed_value_message(entry_index, changed_values, item_type, old_value, new_value) else: while n < len( selected_val[item_type][entry_pair][depend_value]): new_value = list(selected_val[item_type][entry_pair] [depend_value])[n] new_animation_name = self.ean.animations[new_value].name \ if item_type == Animation and new_value < len(self.ean.animations) else '' if item_type != Animation or not any( word in new_animation_name for word in BLACKLISTED_WORDS) \ or new_animation_name.endswith(entry_name): if not new_animation_name.endswith( entry_name) and self.find_conflict( item_type, entry_pair, depend_value, selected_data, new_value): new_value = self.create_new_index(item_type) self.changed_value_message( entry_index, changed_values, item_type, old_value, new_value) else: self.changed_value_message( entry_index, changed_values, item_type, old_value, new_value, False) break n += 1 else: new_value = self.create_new_index(item_type) self.changed_value_message(entry_index, changed_values, item_type, old_value, new_value) else: new_value = self.create_new_index(item_type) self.changed_value_message(entry_index, changed_values, item_type, old_value, new_value) # Copy EAN/BDM entries if not self.copy_index(item_type, old_value, new_value): return False self.links[item_type][entry_pair][depend_value][ old_value] = new_value n += 1 return True def on_paste(self, _): if not self.parent.copied: with wx.MessageDialog( self, f'No entries are copied from the right panel to Paste' ) as dlg: dlg.ShowModal() return selected = self.entry_list.GetSelections() if not selected: with wx.MessageDialog( self, f'No entries are selected to Paste onto') as dlg: dlg.ShowModal() return copied = pickle.loads(self.parent.copied) # Cut length of selected to match copied copy_length = len(copied) selected_length = len(selected) if selected_length > copy_length: for item in selected[copy_length:]: self.entry_list.Unselect(item) selected = selected[:copy_length] item = selected[-1] self.entry_list.Select(item) # Increase length to match selected for n in range(copy_length - selected_length): item = self.entry_list.GetNextItem(item) if not item.IsOk(): with wx.MessageDialog( self, f'Not enough entries to paste over. Expected {copy_length}' ) as dlg: dlg.ShowModal() return self.entry_list.Select(item) selected.append(item) selected_data = [ self.entry_list.GetItemData(item) for item in selected ] # Warn about changing multiple entries if len(copied) > 1: msg = '' for n, copied_data in enumerate(copied): msg += f' * {selected_data[n].index} -> {copied_data.index}\n' with MultiMessageDialog( self, 'Are you sure you want to replace the following entries?', 'Warning', msg, wx.YES | wx.NO) as dlg: if dlg.ShowModal() != wx.ID_YES: return # Paste entries selected_values = [] copied_values = [] changed_values = defaultdict(list) for n, copied_data in enumerate(copied): selected_values.append( (selected_data[n].index, selected_data[n].get_static_values())) copied_values.append(copied_data.get_static_values()) for selected_val, copied_val in zip(selected_values, copied_values): # Example: # Item type: Animation # entry: Index # dependency: Type # depend_value: 5 # entry_values: {1, 2, 3} for item_type, v1 in copied_val.items(): if item_type not in [Animation, Hitbox, Camera]: continue for entry_pair, v2 in v1.items(): for depend_value, entry_values in v2.items(): # Skip if dependency isn't a character if item_type.dependencies[entry_pair][ depend_value] != 'Character': continue if not self.get_changed_values( changed_values, item_type, entry_pair, depend_value, entry_values, selected_val, selected_data): return # Finally copy BAC Entries for n, copied_data in enumerate(copied): entry = self.entry_list.GetItemData(selected[n]) entry.paste(copied_data, self.links) # Display message msg = f'Pasted {len(copied)} entry(s)' pub.sendMessage('set_status_bar', text=msg) with ChangedDialog(self, changed_values) as dlg: dlg.ShowModal() # changed_msg = '' # for item_type, values in changed_values.items(): # if item_type == Animation: # changed_msg += f'{self.code}.ean -> {self.parent.side_panel.code}.ean\n' # elif item_type == Hitbox: # changed_msg += f'{self.code}_PLAYER.bdm -> {self.parent.side_panel.code}_PLAYER.bdm\n' # elif item_type == Camera: # changed_msg += f'{self.code}.cam.ean -> {self.parent.side_panel.code}.cam.ean\n' # else: # raise(TypeError(f'Unsupported type: {item_type.__name__}')) # changed_msg += ''.join(sorted(list(zip(*values))[1])) # with MultiMessageDialog( # self, 'The following entries in the listed files have been changed:', msg, changed_msg, wx.OK) as dlg: # dlg.ShowModal() def on_add(self, _): if not self.parent.copied: with wx.MessageDialog( self, f'No entries are copied from the right panel to Add' ) as dlg: dlg.ShowModal() return copied = pickle.loads(self.parent.copied) selected_data = [item for item in copied] # Paste entries selected_values = [] copied_values = [] changed_values = defaultdict(list) for n, copied_data in enumerate(copied): selected_values.append( (selected_data[n].index, selected_data[n].get_static_values())) copied_values.append(copied_data.get_static_values()) # same code as in on_paste(), but it compares to the added entry (i really have no idea why this works..) for selected_val, copied_val in zip(selected_values, copied_values): for item_type, v1 in copied_val.items(): if item_type not in [Animation, Hitbox, Camera]: continue for entry_pair, v2 in v1.items(): for depend_value, entry_values in v2.items(): # Skip if dependency isn't a character if item_type.dependencies[entry_pair][ depend_value] != 'Character': continue if not self.get_changed_values( changed_values, item_type, entry_pair, depend_value, entry_values, selected_val, selected_data): return # Add BAC Entry for n, copied_data in enumerate(copied): # add new entries to the end so we don't override important CMN entries index = len(self.bac.entries) new_entry = Entry(self.bac, index) root = self.entry_list.GetRootItem() new_entry.paste(copied_data, self.links) self.bac.entries.append(new_entry) self.entry_list.AppendItem( root, f'{new_entry.index}: {KNOWN_ENTRIES.get(new_entry.index, "Unknown")}', data=new_entry) # Display message msg = f'Added {len(copied)} entry(s)' pub.sendMessage('set_status_bar', text=msg) with ChangedDialog(self, changed_values) as dlg: dlg.ShowModal()