import tkinter as tk from tkinter import messagebox from tkinter import ttk import tkinter.font as tkfont import json from tkinter import filedialog class CreditsApp: def __init__(self, root): self.root = root self.root.title("Credits Script Creator") # Canvas dimensions self.canvas_width = 256 self.canvas_height = 192 # Initialize pages self.pages = {} self.current_page = 0 # Currently selected text item self.selected_item = None self.dragging = False # Guide lines settings self.show_guides = tk.BooleanVar(value=False) self.guide_color = tk.StringVar(value='black') self.vertical_lines = tk.IntVar(value=0) self.horizontal_lines = tk.IntVar(value=0) # Define fonts self.normal_font = tkfont.Font(family='TkDefaultFont', size=10) self.bold_font = tkfont.Font(family='TkDefaultFont', size=10, weight='bold') # Create UI elements self.create_widgets() # Bind arrow keys self.root.bind('', self.prev_page) self.root.bind('', self.next_page) def create_widgets(self): # Frame for canvas and controls self.frame = ttk.Frame(self.root) self.frame.pack(padx=10, pady=10) # Canvas self.canvas = tk.Canvas(self.frame, width=self.canvas_width, height=self.canvas_height, bg='white') self.canvas.pack() self.canvas.bind("", self.on_canvas_click) self.canvas.bind("", self.on_canvas_press) self.canvas.bind("", self.on_canvas_drag) self.canvas.bind("", self.on_canvas_release) # Page label self.page_label = ttk.Label(self.frame, text=f"Page: {self.current_page}") self.page_label.pack() # Page navigation buttons nav_frame = ttk.Frame(self.frame) nav_frame.pack(pady=5) self.prev_button = ttk.Button(nav_frame, text="Previous Page", command=self.prev_page) self.prev_button.pack(side=tk.LEFT) self.next_button = ttk.Button(nav_frame, text="Next Page", command=self.next_page) self.next_button.pack(side=tk.LEFT) # Add Text button self.add_text_button = ttk.Button(self.frame, text="Add Text", command=self.add_text_center) self.add_text_button.pack(side=tk.LEFT, pady=5) # Save and Load buttons self.save_button = ttk.Button(self.frame, text="Save", command=self.save_data) self.save_button.pack(side=tk.LEFT, padx=5) self.load_button = ttk.Button(self.frame, text="Load", command=self.load_data) self.load_button.pack(side=tk.LEFT, padx=5) # Generate button self.generate_button = ttk.Button(self.frame, text="Generate CPP Code", command=self.generate_cpp_code) self.generate_button.pack(side=tk.LEFT, pady=5) # Warning label for character limit self.warning_label = ttk.Label(self.frame, text="", foreground="red") self.warning_label.pack() # Guide lines controls guide_frame = ttk.Frame(self.root) guide_frame.pack(padx=10, pady=5, fill=tk.X) # Show guides checkbox self.show_guides_checkbox = ttk.Checkbutton(guide_frame, text="Show Guide Lines", variable=self.show_guides, command=self.refresh_canvas) self.show_guides_checkbox.grid(row=0, column=0, sticky=tk.W) # Guide color combobox ttk.Label(guide_frame, text="Guide Line Color:").grid(row=0, column=1, sticky=tk.W) self.guide_color_combo = ttk.Combobox(guide_frame, textvariable=self.guide_color, values=['black', 'light grey', 'pink'], state='readonly') self.guide_color_combo.grid(row=0, column=2, sticky=tk.W) self.guide_color_combo.bind("<>", lambda e: self.refresh_canvas()) # Vertical lines spinbox ttk.Label(guide_frame, text="Vertical Lines:").grid(row=1, column=0, sticky=tk.W) self.vertical_lines_spin = ttk.Spinbox(guide_frame, from_=0, to=10, textvariable=self.vertical_lines, width=5, command=self.refresh_canvas) self.vertical_lines_spin.grid(row=1, column=1, sticky=tk.W) self.vertical_lines_spin.bind("", lambda e: self.refresh_canvas()) # Horizontal lines spinbox ttk.Label(guide_frame, text="Horizontal Lines:").grid(row=1, column=2, sticky=tk.W) self.horizontal_lines_spin = ttk.Spinbox(guide_frame, from_=0, to=10, textvariable=self.horizontal_lines, width=5, command=self.refresh_canvas) self.horizontal_lines_spin.grid(row=1, column=3, sticky=tk.W) self.horizontal_lines_spin.bind("", lambda e: self.refresh_canvas()) # Frame for selected item controls self.selected_frame = ttk.Frame(self.root) self.selected_frame.pack(padx=10, pady=10, fill=tk.X) # Variables for selected item properties self.text_var = tk.StringVar() self.palette_var = tk.StringVar() self.start_page_var = tk.IntVar() self.end_page_var = tk.IntVar() self.y_var = tk.IntVar() # Controls for selected item self.create_selected_item_controls() # Initially disable the controls self.disable_selected_item_controls() def create_selected_item_controls(self): # Text label and entry ttk.Label(self.selected_frame, text="Text:").grid(row=0, column=0, sticky=tk.W) self.text_entry = ttk.Entry(self.selected_frame, textvariable=self.text_var) self.text_entry.grid(row=0, column=1, sticky=tk.W+tk.E, columnspan=3) self.text_entry.bind("", self.on_text_change) # Palette label and combobox ttk.Label(self.selected_frame, text="Palette:").grid(row=1, column=0, sticky=tk.W) self.palette_combo = ttk.Combobox(self.selected_frame, textvariable=self.palette_var, values=['Red', 'Blue'], state='readonly') self.palette_combo.grid(row=1, column=1, sticky=tk.W) self.palette_combo.bind("<>", self.on_palette_change) # Y position label and spinbox ttk.Label(self.selected_frame, text="Y:").grid(row=1, column=2, sticky=tk.W) self.y_spin = ttk.Spinbox(self.selected_frame, from_=0, to=self.canvas_height, textvariable=self.y_var, width=5, command=self.on_position_change) self.y_spin.grid(row=1, column=3, sticky=tk.W) self.y_spin.bind("", lambda e: self.on_position_change()) # Start page label and spinbox ttk.Label(self.selected_frame, text="Start Page:").grid(row=2, column=0, sticky=tk.W) self.start_page_spin = ttk.Spinbox(self.selected_frame, from_=0, to=999, textvariable=self.start_page_var, width=5, command=self.on_multipage_change) self.start_page_spin.grid(row=2, column=1, sticky=tk.W) self.start_page_spin.bind("", lambda e: self.on_multipage_change()) # End page label and spinbox ttk.Label(self.selected_frame, text="End Page:").grid(row=2, column=2, sticky=tk.W) self.end_page_spin = ttk.Spinbox(self.selected_frame, from_=0, to=999, textvariable=self.end_page_var, width=5, command=self.on_multipage_change) self.end_page_spin.grid(row=2, column=3, sticky=tk.W) self.end_page_spin.bind("", lambda e: self.on_multipage_change()) # Delete button self.delete_button = ttk.Button(self.selected_frame, text="Delete", command=self.delete_selected_item) self.delete_button.grid(row=3, column=0, columnspan=4, pady=5) # Configure column weights self.selected_frame.columnconfigure(1, weight=1) self.selected_frame.columnconfigure(3, weight=1) def enable_selected_item_controls(self): self.text_entry.config(state='normal') self.palette_combo.config(state='readonly') self.start_page_spin.config(state='normal') self.end_page_spin.config(state='normal') self.delete_button.config(state='normal') self.y_spin.config(state='normal') def disable_selected_item_controls(self): self.text_entry.config(state='disabled') self.palette_combo.config(state='disabled') self.start_page_spin.config(state='disabled') self.end_page_spin.config(state='disabled') self.delete_button.config(state='disabled') self.y_spin.config(state='disabled') def on_canvas_click(self, event): # Left-click selects or deselects items x, y = event.x, event.y tolerance = 2 # Small tolerance for detecting nearby items items = self.canvas.find_overlapping(x - tolerance, y - tolerance, x + tolerance, y + tolerance) item_clicked = None for item in items: tags = self.canvas.gettags(item) if 'text_item' in tags: item_clicked = item break if item_clicked: # Select the text item self.selected_item = item_clicked self.highlight_selected_item() # Bring the selected item to the front self.canvas.tag_raise(self.selected_item) # Update selected item controls self.update_selected_item_controls() else: # Deselect any previously selected item self.selected_item = None self.highlight_selected_item() self.disable_selected_item_controls() def update_selected_item_controls(self): # Enable controls self.enable_selected_item_controls() # Get the entry corresponding to the selected item entry = self.get_entry_by_canvas_item(self.selected_item) if not entry: return # Update variables self.text_var.set(entry['text']) self.palette_var.set(entry['palette']) self.start_page_var.set(entry['start_page']) self.end_page_var.set(entry['end_page']) self.y_var.set(int(entry['y'])) def on_text_change(self, event): if self.selected_item: entry = self.get_entry_by_canvas_item(self.selected_item) if not entry: return new_text = self.text_var.get() # Disallow numbers if any(char.isdigit() for char in new_text): messagebox.showerror("Error", "Text cannot contain numbers.") self.text_var.set(entry['text']) return # Enforce rules if len(new_text) > 41: messagebox.showerror("Error", "Text exceeds 41 characters limit.") self.text_var.set(entry['text']) return if new_text.startswith(' ') or new_text.startswith('\0'): messagebox.showerror("Error", "Text cannot start with a space or null terminator.") self.text_var.set(entry['text']) return # Update the text in the data structure entry['text'] = new_text # Update the text on the canvas self.canvas.itemconfig(self.selected_item, text=new_text) # Update character warning self.update_character_warning() def on_palette_change(self, event): if self.selected_item: entry = self.get_entry_by_canvas_item(self.selected_item) if not entry: return new_palette = self.palette_var.get() entry['palette'] = new_palette # Update the text color on the canvas self.canvas.itemconfig(self.selected_item, fill=new_palette.lower()) def on_multipage_change(self): if self.selected_item: entry = self.get_entry_by_canvas_item(self.selected_item) if not entry: return try: start_page = int(self.start_page_var.get()) end_page = int(self.end_page_var.get()) if start_page > end_page: messagebox.showerror("Error", "Start page cannot be greater than end page.") self.start_page_var.set(entry['start_page']) self.end_page_var.set(entry['end_page']) return entry['start_page'] = start_page entry['end_page'] = end_page entry['multipage'] = True if start_page != end_page else False # Update the pages where this entry should appear self.update_entry_pages(entry) # Refresh canvas to show/hide the entry on the current page self.refresh_canvas() except ValueError: messagebox.showerror("Error", "Invalid page number.") def on_position_change(self): if self.selected_item: entry = self.get_entry_by_canvas_item(self.selected_item) if not entry: return try: new_y = int(self.y_var.get()) entry['y'] = new_y # Update the item's position on the canvas self.canvas.coords(self.selected_item, self.canvas_width / 2, new_y) except ValueError: messagebox.showerror("Error", "Invalid Y coordinate.") def on_canvas_press(self, event): # Start dragging if a text item is under the cursor x, y = event.x, event.y tolerance = 2 items = self.canvas.find_overlapping(x - tolerance, y - tolerance, x + tolerance, y + tolerance) for item in items: tags = self.canvas.gettags(item) if 'text_item' in tags: self.selected_item = item self.highlight_selected_item() self.dragging = True self.drag_start_y = event.y # Bring the selected item to the front self.canvas.tag_raise(self.selected_item) # Update selected item controls self.update_selected_item_controls() break def on_canvas_drag(self, event): if self.dragging and self.selected_item: dy = event.y - self.drag_start_y self.canvas.move(self.selected_item, 0, dy) self.drag_start_y = event.y # Update the position in the data structure self.update_entry_position(self.selected_item, dy) # Update the Y variable entry = self.get_entry_by_canvas_item(self.selected_item) if entry: self.y_var.set(int(entry['y'])) def on_canvas_release(self, event): self.dragging = False def highlight_selected_item(self): # Remove highlight from all items by setting normal font for item in self.canvas.find_withtag('text_item'): self.canvas.itemconfig(item, font=self.normal_font) if self.selected_item: # Highlight the selected item by setting bold font self.canvas.itemconfig(self.selected_item, font=self.bold_font) def delete_selected_item(self): # Remove the item from the canvas and data structures if not self.selected_item: return entry = self.get_entry_by_canvas_item(self.selected_item) if not entry: return # Remove from all pages for page in range(entry['start_page'], entry['end_page'] + 1): if page in self.pages and entry in self.pages[page]: self.pages[page].remove(entry) self.canvas.delete(self.selected_item) self.selected_item = None self.highlight_selected_item() self.disable_selected_item_controls() # Update character warning self.update_character_warning() def add_text_center(self): # Add text at the center of the canvas y = self.canvas_height / 2 # Create a new text entry with default properties self.create_text_entry(y) def create_text_entry(self, y): # Set default properties text = "New Text" palette = "Red" start_page = self.current_page end_page = self.current_page multipage = False # Enforce rules if len(text) > 41: messagebox.showerror("Error", "Text exceeds 41 characters limit.") return if text.startswith(' ') or text.startswith('\0'): messagebox.showerror("Error", "Text cannot start with a space or null terminator.") return # Create the text item on the canvas, centered horizontally text_item = self.canvas.create_text(self.canvas_width / 2, y, text=text, anchor=tk.CENTER, fill=palette.lower(), tags=('text_item',), font=self.bold_font) # Add entry to pages entry = { 'text': text, 'y': y, 'palette': palette, 'multipage': multipage, 'start_page': start_page, 'end_page': end_page, 'canvas_item': text_item } self.update_entry_pages(entry) # Select the new text item self.selected_item = text_item self.highlight_selected_item() self.update_selected_item_controls() # Refresh canvas to show the new text self.refresh_canvas() def update_entry_pages(self, entry): # Remove entry from all pages first for page_entries in self.pages.values(): if entry in page_entries: page_entries.remove(entry) # Add entry to specified pages for page in range(entry['start_page'], entry['end_page'] + 1): if page not in self.pages: self.pages[page] = [] self.pages[page].append(entry) def update_entry_position(self, canvas_item, dy): # Update the position of the entry in the data structure entry = self.get_entry_by_canvas_item(canvas_item) if not entry: return entry['y'] += dy def get_entry_by_canvas_item(self, canvas_item): # Find the entry corresponding to the canvas item for page_entries in self.pages.values(): for entry in page_entries: if entry.get('canvas_item') == canvas_item: return entry return None def prev_page(self, event=None): if self.current_page > 0: self.current_page -= 1 self.page_label.config(text=f"Page: {self.current_page}") self.refresh_canvas() def next_page(self, event=None): self.current_page += 1 self.page_label.config(text=f"Page: {self.current_page}") self.refresh_canvas() def draw_guides(self): if not self.show_guides.get(): return color = self.guide_color.get() num_v_lines = self.vertical_lines.get() num_h_lines = self.horizontal_lines.get() # Draw vertical lines if num_v_lines > 0: v_spacing = self.canvas_width / (num_v_lines + 1) for i in range(1, num_v_lines + 1): x = v_spacing * i self.canvas.create_line(x, 0, x, self.canvas_height, fill=color, dash=(2, 4)) # Draw horizontal lines if num_h_lines > 0: h_spacing = self.canvas_height / (num_h_lines + 1) for i in range(1, num_h_lines + 1): y = h_spacing * i self.canvas.create_line(0, y, self.canvas_width, y, fill=color, dash=(2, 4)) def refresh_canvas(self): self.canvas.delete("all") if self.current_page in self.pages: entries = self.pages[self.current_page] # Sort entries by Y-value entries = sorted(entries, key=lambda e: e['y']) for entry in entries: # Determine font based on selection font = self.bold_font if self.selected_item and self.selected_item == entry.get('canvas_item') else self.normal_font text_item = self.canvas.create_text(self.canvas_width / 2, entry['y'], text=entry['text'], anchor=tk.CENTER, fill=entry['palette'].lower(), tags=('text_item',), font=font) # Keep track of the canvas item entry['canvas_item'] = text_item # Update selected_item reference if necessary if self.selected_item == entry.get('canvas_item'): self.selected_item = text_item # Draw guide lines if enabled self.draw_guides() self.highlight_selected_item() # Update character warning self.update_character_warning() def calculate_total_chars(self, page): if page not in self.pages: return 0 total = 0 for entry in self.pages[page]: total += len(entry['text'].replace(' ', '')) return total def update_character_warning(self): total_chars = self.calculate_total_chars(self.current_page) if total_chars > 128: self.warning_label.config(text=f"Warning: Total characters on page exceed 128 (Current: {total_chars})") else: self.warning_label.config(text="") def generate_cpp_code(self): code_lines = [] code_lines.append('#include "nsmb.hpp"\n') code_lines.append('\n') code_lines.append('ncp_over(0x020EA678, 8)') code_lines.append('static constexpr End::ScriptEntry script[] = {\n') entries_processed = set() # Collect entries, sorted by page number pages = sorted(self.pages.keys()) for page_num in pages: page_entries = self.pages[page_num] entries_to_generate = [] # Collect entries that are on this page for entry in page_entries: key = (entry['text'], entry['y'], entry['start_page'], entry['end_page']) if key not in entries_processed: entries_processed.add(key) if entry['multipage']: # Generate an entry for each page in the multipage range for page in range(entry['start_page'], entry['end_page'] + 1): if page not in self.pages: continue if page == page_num: entries_to_generate.append((entry, page)) else: if entry['start_page'] == page_num: entries_to_generate.append((entry, page_num)) else: if entry['multipage'] and entry['start_page'] <= page_num <= entry['end_page']: entries_to_generate.append((entry, page_num)) if not entries_to_generate: continue # Skip pages with no entries code_lines.append(f'\t// Page {page_num}') # Sort entries by Y-value sorted_entries = sorted(entries_to_generate, key=lambda e: e[0]['y']) for entry, page in sorted_entries: is_multipage = 'true' if (entry['multipage'] and page < entry['end_page']) else 'false' code_lines.append(f'\t{{"{entry["text"]}"end, {page}, {int(entry["y"])}, End::ScriptEntry::{entry["palette"]}, {is_multipage}}},') code_lines.append('\tEnd::ScriptTerminator, // Terminator') code_lines.append('};\n') code_lines.append('static_assert(NTR_ARRAY_SIZE(script) < 115, "Script size out of bounds");') # Show the generated code in a new window code_window = tk.Toplevel(self.root) code_window.title("Generated CPP Code") code_text = tk.Text(code_window, wrap='none') code_text.pack(expand=True, fill='both') code_text.insert('1.0', '\n'.join(code_lines)) # Add a scrollbar scroll_x = tk.Scrollbar(code_window, orient='horizontal', command=code_text.xview) scroll_x.pack(side='bottom', fill='x') code_text.configure(xscrollcommand=scroll_x.set) scroll_y = tk.Scrollbar(code_window, orient='vertical', command=code_text.yview) scroll_y.pack(side='right', fill='y') code_text.configure(yscrollcommand=scroll_y.set) def save_data(self): file_path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON files", "*.json")]) if file_path: # Convert entries to serializable format serializable_pages = {} for page, entries in self.pages.items(): serializable_entries = [] for entry in entries: serializable_entry = entry.copy() if 'canvas_item' in serializable_entry: del serializable_entry['canvas_item'] # Remove non-serializable item serializable_entries.append(serializable_entry) serializable_pages[page] = serializable_entries with open(file_path, 'w') as f: json.dump(serializable_pages, f, indent=4) def load_data(self): file_path = filedialog.askopenfilename(filetypes=[("JSON files", "*.json")]) if file_path: with open(file_path, 'r') as f: loaded_pages = json.load(f) # Convert page keys to integers self.pages = {} for page_str, entries in loaded_pages.items(): page_num = int(page_str) self.pages[page_num] = [] for entry in entries: # Ensure numeric fields are correct types entry['y'] = float(entry['y']) entry['start_page'] = int(entry['start_page']) entry['end_page'] = int(entry['end_page']) entry['multipage'] = bool(entry['multipage']) entry['canvas_item'] = None # Will be set during refresh self.pages[page_num].append(entry) # Clear current selection and refresh canvas self.selected_item = None self.disable_selected_item_controls() self.refresh_canvas() # Run the app if __name__ == '__main__': root = tk.Tk() app = CreditsApp(root) root.mainloop()