forked from DRGN-DRC/Melee-Modding-Wizard
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcodesManager.py
3268 lines (2514 loc) · 139 KB
/
codesManager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python
# This file's encoding: UTF-8, so that non-ASCII characters can be used in strings.
#
# ███╗ ███╗ ███╗ ███╗ ██╗ ██╗ ------- -------
# ████╗ ████║ ████╗ ████║ ██║ ██║ # -=======---------------------------------------------------=======- #
# ██╔████╔██║ ██╔████╔██║ ██║ █╗ ██║ # ~ ~ Written by DRGN of SmashBoards (Daniel R. Cappel); May, 2020 ~ ~ #
# ██║╚██╔╝██║ ██║╚██╔╝██║ ██║███╗██║ # [ Built with Python v2.7.16 and Tkinter 8.5 ] #
# ██║ ╚═╝ ██║ ██║ ╚═╝ ██║ ╚███╔███╔╝ # -======---------------------------------------------------======- #
# ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ------ ------
# - - Melee Modding Wizard - -
# External Dependencies
import os
import ttk
import time
import copy
import struct
import webbrowser
import tkMessageBox
import tkFileDialog
import Tkinter as Tk
from ScrolledText import ScrolledText
# Internal Dependencies
import globalData
from FileSystem.dol import RevisionPromptWindow
from basicFunctions import grammarfyList, msg, printStatus, openFolder, removeIllegalCharacters, uHex, validHex
from codeMods import CodeMod, ConfigurationTypes, CodeLibraryParser
from guiSubComponents import (
PopupScrolledTextWindow, exportSingleFileWithGui, cmsg, VerticalScrolledFrame, LabelButton, ToolTip, CodeLibrarySelector,
CodeSpaceOptionsWindow, ColoredLabelButton, BasicWindow, DisguisedEntry
)
class CodeManagerTab( ttk.Frame ):
""" GUI to install/uninstall code mods from the currently loaded disc. """
def __init__( self, parent, mainGui ):
ttk.Frame.__init__( self, parent )
# Add this tab to the main GUI, and add drag-and-drop functionality
mainGui.mainTabFrame.add( self, text=' Code Manager ' )
mainGui.dnd.bindtarget( self, mainGui.dndHandler, 'text/uri-list' )
# Create the notebook that code module tabs (categories) will be attached to
self.codeLibraryNotebook = ttk.Notebook( self )
self.codeLibraryNotebook.pack( fill='both', expand=1, pady=7 )
self.codeLibraryNotebook.bind( '<<NotebookTabChanged>>', self.onTabChange )
self.parser = CodeLibraryParser()
self.libraryFolder = ''
self.isScanning = False
self.lastTabSelected = None # Used to prevent redundant onTabChange calls
# Create the control panel
self.controlPanel = ttk.Frame( self, padding="20 8 20 20" ) # Padding: L, T, R, B
# Add the button bar and the Code Library Selection button
buttonBar = ttk.Frame( self.controlPanel )
librarySelectionBtn = ColoredLabelButton( buttonBar, 'books', lambda event: CodeLibrarySelector(globalData.gui.root), 'Init' )
librarySelectionBtn.pack( side='right', padx=6 )
self.libraryToolTipText = Tk.StringVar()
self.libraryToolTipText.set( 'Click to change Code Library.\n\nCurrent library:\n' + globalData.getModsFolderPath() )
#ToolTip( librarySelectionBtn, delay=900, justify='center', location='w', textvariable=self.libraryToolTipText, wraplength=600, offset=-10 )
librarySelectionBtn.toolTip.configure( textvariable=self.libraryToolTipText, delay=900, justify='center', location='w', wraplength=600, offset=-10 )
# Add the Settings button
self.overwriteOptionsBtn = ColoredLabelButton( buttonBar, 'gear', lambda event: CodeSpaceOptionsWindow(globalData.gui.root), 'Edit Code-Space Options' )
if not globalData.disc:
self.overwriteOptionsBtn.disable( 'Load a disc to edit these settings.' )
self.overwriteOptionsBtn.pack( side='right', padx=6 )
self.overwriteOptionsBtn.toolTip.configure( delay=900, justify='center', location='w', wraplength=600, offset=-10 )
buttonBar.pack( fill='x', pady=(5, 20) )
# Begin adding primary buttons
self.openMcmFileBtn = ttk.Button( self.controlPanel, text='Open this File', command=self.openLibraryFile, state='disabled' )
self.openMcmFileBtn.pack( pady=4, padx=6, ipadx=8 )
ttk.Button( self.controlPanel, text='Open Mods Library Folder', command=self.openLibraryFolder ).pack( pady=4, padx=6, ipadx=8 )
ttk.Separator( self.controlPanel, orient='horizontal' ).pack( pady=7, ipadx=100 )
saveButtonsContainer = ttk.Frame( self.controlPanel, padding="0 0 0 0" )
self.saveBtn = ttk.Button( saveButtonsContainer, text='Save', state='disabled', command=globalData.gui.save, width=14 )
self.saveBtn.pack( side='left', padx=6 )
self.saveAsBtn = ttk.Button( saveButtonsContainer, text='Save As', state='disabled', command=globalData.gui.saveAs, width=14 )
self.saveAsBtn.pack( side='left', padx=6 )
saveButtonsContainer.pack( pady=4 )
ttk.Separator( self.controlPanel, orient='horizontal' ).pack( pady=7, ipadx=110 )
createFileContainer = ttk.Frame( self.controlPanel, padding="0 0 0 0" )
ttk.Button( createFileContainer, text='Create INI', command=self.saveIniFile, width=12 ).pack( side='left', padx=6 )
ttk.Button( createFileContainer, text='Create GCT', command=self.saveGctFile, width=12 ).pack( side='left', padx=6 )
createFileContainer.pack( pady=4 )
ttk.Separator( self.controlPanel, orient='horizontal' ).pack( pady=7, ipadx=100 )
self.restoreBtn = ttk.Button( self.controlPanel, text='Restore Vanilla DOL', state='disabled', command=self.askRestoreDol, width=23 )
self.restoreBtn.pack( pady=4 )
self.exportBtn = ttk.Button( self.controlPanel, text='Export DOL', state='disabled', command=self.exportDOL, width=23 )
self.exportBtn.pack( pady=4 )
ttk.Separator( self.controlPanel, orient='horizontal' ).pack( pady=7, ipadx=100 )
# Add the Select/Deselect All buttons
selectBtnsContainer = ttk.Frame( self.controlPanel, padding="0 0 0 0" )
selectBtnsContainer.selectAllBtn = ttk.Button( selectBtnsContainer, text='Select All', width=12 )
selectBtnsContainer.selectAllBtn.pack( side='left', padx=6, pady=0 )
ToolTip( selectBtnsContainer.selectAllBtn, delay=600, justify='center', text='Shift-click to select\nwhole library' )
selectBtnsContainer.selectAllBtn.bind( '<Button-1>', self.selectAllMods )
selectBtnsContainer.selectAllBtn.bind( '<Shift-Button-1>', self.selectWholeLibrary )
selectBtnsContainer.deselectAllBtn = ttk.Button( selectBtnsContainer, text='Deselect All', width=12 )
selectBtnsContainer.deselectAllBtn.pack( side='left', padx=6, pady=0 )
ToolTip( selectBtnsContainer.deselectAllBtn, delay=600, justify='center', text='Shift-click to deselect\nwhole library' )
selectBtnsContainer.deselectAllBtn.bind( '<Button-1>', self.deselectAllMods )
selectBtnsContainer.deselectAllBtn.bind( '<Shift-Button-1>', self.deselectWholeLibrary )
selectBtnsContainer.pack( pady=4 )
ttk.Button( self.controlPanel, text=' Rescan for Mods ', command=self.scanCodeLibrary ).pack( pady=4 )
# Add a label that shows how many code modes are selected on the current tab
self.installTotalLabel = Tk.StringVar()
self.installTotalLabel.set( '' )
ttk.Label( self.controlPanel, textvariable=self.installTotalLabel ).pack( side='bottom' )
self.bind( '<Configure>', self.alignControlPanel )
def enableControls( self ):
""" Set button enable/disable states. """
self.saveBtn['state'] = 'normal'
self.saveAsBtn['state'] = 'normal'
self.restoreBtn['state'] = 'normal'
self.exportBtn['state'] = 'normal'
def onTabChange( self, event=None, forceUpdate=False ):
""" Called whenever the selected tab in the library changes, or when a new tab is added. """
# Check if the Code Manager tab is selected, and thus if any updates are really needed
if not forceUpdate and globalData.gui.root.nametowidget( globalData.gui.mainTabFrame.select() ) != self:
return
currentTab = self.getCurrentTab()
if not forceUpdate and self.lastTabSelected == currentTab:
print( 'already selected;', self.controlPanel.winfo_manager(), self.controlPanel.winfo_ismapped() )
return
elif self.lastTabSelected and self.lastTabSelected != currentTab:
globalData.gui.playSound( 'menuChange' )
# Prevent focus on the tabs themselves (prevents appearance of selection box)
# currentTab = globalData.gui.root.nametowidget( self.codeLibraryNotebook.select() )
# currentTab.focus()
#print( 'tab changed; called with event:', event )
#time.sleep(2)
# Remove existing ModModules, and only add those for the currently selected tab
self.emptyModsPanels()
self.createModModules( currentTab )
self.alignControlPanel( currentTab=currentTab )
self.updateInstalledModsTabLabel( currentTab )
self.lastTabSelected = currentTab
def emptyModsPanels( self, notebook=None ):
""" Destroys all GUI elements (ModModules) for all Code Library tabs.
Does not destroy their containers, so they can be repopulated on tab change. """
root = globalData.gui.root
if not notebook:
notebook = self.codeLibraryNotebook
for tabName in notebook.tabs():
tabWidget = root.nametowidget( tabName )
if tabWidget.winfo_class() == 'TFrame':
modsPanel = tabWidget.winfo_children()[0] # The VSF
# Detatch the GUI module from the mod object
for mod in modsPanel.mods:
mod.guiModule = None
# Delete the GUI module
for childWidget in modsPanel.interior.winfo_children(): # Avoiding .clear method to avoid resetting scroll position
childWidget.destroy()
else:
self.emptyModsPanels( tabWidget )
self.lastTabSelected = None # Allows the next onTabChange to proceed if this was called independently of it
def createModModules( self, currentTab ):
""" Creates GUI elements (ModModules) and populates them in the Code Library tab currently in view. """
foundMcmFormatting = False
if currentTab:
modsPanel = currentTab.winfo_children()[0] # The VSF
for mod in modsPanel.mods:
module = ModModule( modsPanel.interior, mod )
module.pack( fill='x', expand=1 )
if not mod.isAmfs:
foundMcmFormatting = True
# Enable or disable the 'Open this file' button
if foundMcmFormatting:
self.openMcmFileBtn['state'] = 'normal'
else:
self.openMcmFileBtn['state'] = 'disabled'
def alignControlPanel( self, event=None, currentTab=None ):
""" Updates the alignment/position of the control panel (to the right of mod lists) and the global scroll target.
Using this alignment technique rather than just dividing the Code Manager tab into two columns allows the
library tabs to span the entire width of the program, rather than just the left side. """
# Check if the Code Manager tab is selected (and thus if the control panel should be visible)
# if globalData.gui.root.nametowidget( globalData.gui.mainTabFrame.select() ) != self:
# self.controlPanel.place_forget() # Removes the control panel from GUI, without deleting it
# #print( 'removing control panel' )
# return
#print( 'aligning control panel; called with event:', (event) )
if not currentTab:
currentTab = self.getCurrentTab()
if currentTab:
# Get the VerticalScrolledFrame of the currently selected tab
modsPanel = currentTab.winfo_children()[0]
# Get the new coordinates for the control panel frame
currentTab.update_idletasks() # Force the GUI to update in order to get correct new widget positions & sizes.
currentTabWidth = currentTab.winfo_width()
#print( 'placing control panel with tab', currentTab )
self.controlPanel.place( in_=currentTab, x=currentTabWidth * .60, width=currentTabWidth * .40, height=modsPanel.winfo_height() )
else:
# Align and place according to the main library notebook instead
#print( 'placing control panel with topLevel notebook' )
notebookWidth = self.codeLibraryNotebook.winfo_width()
self.controlPanel.place( in_=self.codeLibraryNotebook, x=notebookWidth * .60, width=notebookWidth * .40, height=self.codeLibraryNotebook.winfo_height() )
def getModModules( self, tab ):
""" Get the GUI elements/modules for mods on the given tab. """
scrollingFrame = tab.winfo_children()[0] # VerticalScrolledFrame widget
return scrollingFrame.interior.winfo_children()
def updateInstalledModsTabLabel( self, currentTab=None ):
""" Updates the installed mods count at the bottom of the control panel. """
if not currentTab:
currentTab = self.getCurrentTab()
if not currentTab:
print( '.updateInstalledModsTabLabel() unable to get a current tab.' )
return
modules = self.getModModules( currentTab )
# Count the mods enabled or selected for installation
thisTabSelected = 0
for modModule in modules:
if modModule.mod.state == 'enabled' or modModule.mod.state == 'pendingEnable':
thisTabSelected += 1
# Check total selected mods
librarySelected = 0
for mod in globalData.codeMods:
if mod.state == 'enabled' or mod.state == 'pendingEnable':
librarySelected += 1
self.installTotalLabel.set( 'Enabled on this tab: {} / {}\nEnabled in library: {} / {}'.format(thisTabSelected, len(modules), librarySelected, len(globalData.codeMods)) )
def clear( self ):
""" Clears code mod data containers and the Code Manager tab's GUI (removes buttons and deletes mod modules) """
# Clear data containers for code mod info
self.parser.codeMods = []
self.parser.modNames.clear()
globalData.codeMods = []
globalData.standaloneFunctions = {}
# Delete all mod modules currently populated in the GUI (by deleting the associated tab),
# and remove any other current widgets/labels in the main notebook
for child in self.codeLibraryNotebook.winfo_children():
child.destroy()
self.installTotalLabel.set( '' )
self.lastTabSelected = None # Allows the next onTabChange to proceed if this was called independently of it
def _reattachTabChangeHandler( self, notebook ):
""" Even though the onTabChange event handler is unbound in .scanCodeLibrary(), several
events will still be triggered, and will linger until the GUI's thread can get back
to them. When that happens, if the tab change handler has been re-binded, the handler
will be called for each event (even if they occurred while the handler was not binded.
Thus, this method should be called after idle tasks from the main gui (which includes
the tab change events) have finished. """
notebook.bind( '<<NotebookTabChanged>>', self.onTabChange )
def getCurrentTab( self ):
""" Returns the currently selected tab in the Mods Library tab, or None if one is not selected.
Recursively scans nested folders (notebooks) until the lowest level tab (a frame) is found.
The returned widget is the upper-most ttk.Frame in the tab (exists for placement purposes),
not the VerticalScrolledFrame. To get that, use .winfo_children()[0] on the returned frame. """
if self.codeLibraryNotebook.tabs() == ():
return None
root = globalData.gui.root
selectedTab = root.nametowidget( self.codeLibraryNotebook.select() ) # Will be the highest level tab (either a notebook or placementFrame)
# If the child widget is not a frame, it's a notebook, meaning this represents a directory, and contains more tabs within it
while selectedTab.winfo_class() != 'TFrame':
if selectedTab.tabs() == (): return None
selectedTab = root.nametowidget( selectedTab.select() )
return selectedTab
def getAllTabs( self, notebook=None, tabsList=None ):
""" Returns all Code Library tabs. This will be a list of all of the upper-most ttk.Frame
widgets in the tabs (exists for placement purposes), not the VerticalScrolledFrame. """
root = globalData.gui.root
if not notebook:
notebook = self.codeLibraryNotebook
tabsList = []
for tabName in notebook.tabs():
tabWidget = root.nametowidget( tabName )
if tabWidget.winfo_class() == 'TFrame':
tabsList.append( tabWidget )
else: # It's a notebook; we have to go deeper
self.getAllTabs( tabWidget, tabsList )
return tabsList
def selectCodeLibraryTab( self, targetTabWidget, notebook=None ):
""" Recursively selects all tabs/sub-tabs within the Code Library required to ensure the given target tab is visible. """
if not notebook: # Initial condition; top-level search start
notebook = self.codeLibraryNotebook
self.lastTabSelected = None
else:
# Minimize calls to onTabChange by unbinding the event handler (which will fire throughout this method)
notebook.unbind( '<<NotebookTabChanged>>' )
root = globalData.gui.root
found = False
for tabName in notebook.tabs():
tabWidget = root.nametowidget( tabName ) # This will be the tab's frame widget (placementFrame).
# Check if this is the target tab, if not, check if the target tab is in a notebook sub-tab of this tab
if tabWidget == targetTabWidget: found = True
elif tabWidget.winfo_class() == 'TNotebook': # If it's actually a tab full of mods, the class will be "Frame"
# Check whether this notebook is empty. If not, scan it.
if tabWidget.tabs() == (): continue # Skip this tab.
else: found = self.selectCodeLibraryTab( targetTabWidget, tabWidget )
if found: # Select the current tab
notebook.select( tabWidget )
# If no 'last tab' is stored, this is the lowest-level tab (the target)
if not self.lastTabSelected:
self.lastTabSelected = tabWidget
break
# Wait to let tab change events fizzle out before reattaching the onTabChange event handler
if notebook != self.codeLibraryNotebook:
self.after_idle( self._reattachTabChangeHandler, notebook )
return found
def restartScan( self, playAudio ):
time.sleep( .2 ) # Give a moment to allow for current settings to be saved via saveOptions.
self.isScanning = False
self.parser.stopToRescan = False
self.scanCodeLibrary( playAudio )
def scanCodeLibrary( self, playAudio=True ):
""" The main method to scan (parse) a code library, and then call the methods to scan the DOL and
populate this tab with the mods found. Also defines half of the paths used for .include statements.
The other two .include import paths (CWD and the folder housing each mod text file) will be prepended
to the lists seen here. """
# If this scan is triggered while it is already running, queue/wait for the previous iteration to cancel and re-run
if self.isScanning:
self.parser.stopToRescan = True
return
self.isScanning = True
# Minimize calls to onTabChange by unbinding the event handler (which will fire throughout this method; especially in .clear)
self.codeLibraryNotebook.unbind( '<<NotebookTabChanged>>' )
tic = time.clock()
# Remember the currently selected tab and its scroll position.
currentTab = self.getCurrentTab()
if currentTab:
targetCategory = currentTab.master.tab( currentTab, option='text' )
modsPanel = currentTab.winfo_children()[0]
sliderYPos = modsPanel.vscrollbar.get()[0] # .get() returns e.g. (0.49505277044854884, 0.6767810026385225)
else:
targetCategory = ''
sliderYPos = 0
self.libraryFolder = globalData.getModsFolderPath()
# Validate the current Mods Library folder
if not os.path.exists( self.libraryFolder ):
warningMsg = 'Unable to find this code library:\n\n' + self.libraryFolder + '\n\nClick on the books icon in the top right to select a library.'
ttk.Label( self.codeLibraryNotebook, text=warningMsg, background='white', wraplength=600, justify='center' ).place( relx=0.3, rely=0.5, anchor='s' )
ttk.Label( self.codeLibraryNotebook, image=globalData.gui.imageBank('randall'), background='white' ).place( relx=0.3, rely=0.5, anchor='n', y=10 ) # y not :P
self.isScanning = False
return
self.clear()
# Always parse the Core Code library
# coreCodesLibraryPath = globalData.paths['coreCodes']
# self.parser.includePaths = [ os.path.join(coreCodesLibraryPath, '.include'), os.path.join(globalData.scriptHomeFolder, '.include') ]
# self.parser.processDirectory( coreCodesLibraryPath )
# Parse the currently selected "main" library
#if self.libraryFolder != coreCodesLibraryPath:
self.parser.includePaths = [ os.path.join(self.libraryFolder, '.include'), os.path.join(globalData.scriptHomeFolder, '.include') ]
self.parser.processDirectory( self.libraryFolder )
globalData.codeMods = self.parser.codeMods
# Add the mods parsed above to the GUI
if globalData.codeMods:
self.populateCodeLibraryTabs( targetCategory, sliderYPos )
else: # If no mods are present, add a simple message for the user
warningMsg = 'Unable to find code mods in this library:\n\n' + self.libraryFolder
ttk.Label( self.codeLibraryNotebook, text=warningMsg, background='white', wraplength=600, justify='center' ).place( relx=0.3, rely=0.5, anchor='s' )
ttk.Label( self.codeLibraryNotebook, image=globalData.gui.imageBank('randall'), background='white' ).place( relx=0.3, rely=0.5, anchor='n', y=10 ) # y not :P
# Check once more if another scan is queued. (e.g. if the scan mods button was pressed again while checking for installed mods)
if self.parser.stopToRescan:
self.restartScan( playAudio )
else:
toc = time.clock()
print( 'library parsing time:', toc - tic )
#totalModsInLibraryLabel.set( 'Total Mods in Library: ' + str(len( self.codeModModules )) ) # todo: refactor code to count mods in the modsPanels instead
#totalSFsInLibraryLabel.set( 'Total Standalone Functions in Library: ' + str(len( collectAllStandaloneFunctions(self.codeModModules, forAllRevisions=True) )) )
self.isScanning = False
# Wait to let tab change events fizzle out before reattaching the onTabChange event handler
#self.update_idletasks()
self.after_idle( self._reattachTabChangeHandler, self.codeLibraryNotebook )
#self.onTabChange( forceUpdate=True ) # Make sure it's called at least once
# self.after_idle( self.TEST, 'test1' ) # called in-order
# self.after_idle( self.TEST, 'test2' )
self.after_idle( self.onTabChange, None, True )
if playAudio:
globalData.gui.playSound( 'menuSelect' )
if globalData.disc:
self.enableControls()
def populateCodeLibraryTabs( self, targetCategory='', sliderYPos=0 ):
""" Creates ModModule objects for the GUI, as well as vertical scroll frames/Notebook
widgets needed to house them, and checks for installed mods to set module states. """
notebookWidgets = { '': self.codeLibraryNotebook }
modsPanels = {}
modPanelToScroll = None
# If a disc is loaded, check if the parsed mods are installed in it
if globalData.disc:
globalData.disc.dol.checkForEnabledCodes( globalData.codeMods )
if self.overwriteOptionsBtn.disabled:
self.overwriteOptionsBtn.enable()
#print( '\tThese mods detected as installed:' )
for mod in globalData.codeMods:
parentFolderPath = os.path.dirname( mod.path )
parentFolderName = os.path.split( parentFolderPath )[1]
# Get a path for this mod, relative to the library root (display core codes as relative to root as well)
if parentFolderPath == globalData.paths['coreCodes']: # For the "Core Codes.txt" file
relPath = ''
elif parentFolderName == mod.category:
relPath = ''
else:
relPath = os.path.relpath( parentFolderPath, self.libraryFolder )
if relPath == '.': relPath = ''
modsPanel = modsPanels.get( relPath + '\\' + mod.category )
# Add parent notebooks, if needed, and/or get the parent for this mod
if not modsPanel:
parent = self.codeLibraryNotebook
tabPathParts = []
pathParts = relPath.split( '\\' )
for i, pathItem in enumerate( pathParts ):
tabPathParts.append( pathItem )
thisTabPath = '\\'.join( tabPathParts )
notebook = notebookWidgets.get( thisTabPath )
# Add a new notebook, if needed
if not notebook:
# Create a new tab for this folder or category name
notebook = ttk.Notebook( parent, takefocus=False )
notebook.bind( '<<NotebookTabChanged>>', self.onTabChange )
parent.add( notebook, text=pathItem, image=globalData.gui.imageBank('folderIcon'), compound='left' )
# print( 'adding notebook', notebook._name, 'to', parent._name, 'for', thisTabPath )
notebookWidgets[thisTabPath] = notebook
parent = notebook
# Add a vertical scrolled frame to the last notebook
if i == len( pathParts ) - 1: # Reached the last part (the category)
placementFrame = ttk.Frame( parent ) # This will be the "currentTab" widget returned from .getCurrentTab()
parent.add( placementFrame, text=mod.category )
# Create and add the mods panel (placement frame above needed so we can .place() the mods panel)
modsPanel = VerticalScrolledFrame( placementFrame )
modsPanel.mods = []
#print( 'adding VSF', modsPanel._name, 'to', placementFrame._name, 'for', thisTabPath + '\\' + mod.category )
modsPanel.place( x=0, y=0, relwidth=.60, relheight=1.0 )
modsPanels[relPath + '\\' + mod.category] = modsPanel
# If this is the target panel, Remember it to set its vertical scroll position after all mod modules have been added
if targetCategory == mod.category:
modPanelToScroll = modsPanel
# If this tab is going to be immediately visible/selected, add its modules now
if targetCategory == mod.category:
module = ModModule( modsPanel.interior, mod )
module.pack( fill='x', expand=1 )
# Store the mod for later; actual modules for the GUI will be created on tab selection
modsPanel.mods.append( mod )
# if mod.state == 'enabled':
# print( mod.name )
# If a previous tab and scroll position are desired, set them here
if modPanelToScroll:
self.lastTabSelected = None # Allows the next onTabChange to proceed if this was called independently of it
self.selectCodeLibraryTab( modPanelToScroll.master )
# Update idle tasks so the modPanel's height and scroll position calculate correctly
self.after_idle( lambda y=sliderYPos: modPanelToScroll.canvas.yview_moveto( y ) )
#self.after_idle( self._updateScrollPosition, modPanelToScroll, sliderYPos )
self.updateInstalledModsTabLabel( modPanelToScroll.master )
# Add messages to the background of any empty notebooks
for notebook in notebookWidgets.values():
if not notebook.winfo_children():
warningMsg = 'No code mods found in this folder or category.'
ttk.Label( notebook, text=warningMsg, background='white', wraplength=600, justify='center' ).place( relx=0.3, rely=0.5, anchor='s' )
ttk.Label( notebook, image=globalData.gui.imageBank('randall'), background='white' ).place( relx=0.3, rely=0.5, anchor='n', y=10 ) # y not :P
def _updateScrollPosition( self, modPanel, sliderYPos ):
print( 'updating scroll position' )
self.update_idletasks()
modPanel.canvas.yview_moveto( sliderYPos )
def autoSelectCodeRegions( self ):
""" If 20XX is loaded, this attempts to recognize its version and select the appropriate custom code regions. """
if not globalData.disc:
return
# Check if the loaded DOL is 20XX and get its version
dol = globalData.disc.dol
if not dol.is20XX:
return
v20XX = dol.is20XX.replace( '+', '' ) # Strip from 4.07+/4.07++
# Get the version as major.minor and construct the code regions name
majorMinor = '.'.join( v20XX.split('.')[:2] ) # Excludes version.patch if present (e.g. 5.0.0)
customRegions = '20XXHP {} Regions'.format( majorMinor )
# Check if the current overwrite options match up with the version of 20XX loaded
foundTargetRegions = False
regions = []
for name, boolVar in globalData.overwriteOptions.iteritems():
if boolVar.get(): regions.append( name )
if name == customRegions: foundTargetRegions = True
if not foundTargetRegions:
print( 'Unable to auto-select custom code regions; unsupported 20XX version: {}'.format(v20XX) )
return
# Check that only the one target region is selected
if not regions == [customRegions]:
reselectRegions = tkMessageBox.askyesno( 'Enable Dedicated Region?', 'The game being loaded appears to be for the 20XX Hack Pack, v{}. '
'Would you like to enable the custom code regions specifically for this mod ({})?'
"\n\nIf you're unsure, click yes.".format(v20XX, customRegions) )
if reselectRegions:
# Disable all regions
for boolVar in globalData.overwriteOptions.itervalues():
boolVar.set( False )
# Enable the appropriate region
boolVar = globalData.overwriteOptions.get( customRegions )
if boolVar:
boolVar.set( True )
else:
msg( 'Unable to enable custom code regions for {}; that region could not be '
'found among the configurations in the codeRegionSettings.py file.', 'Custom Code Regions Load Error', error=True )
# Remember these settings
globalData.saveProgramSettings()
def openLibraryFile( self ):
""" Checks if the current tab has a mod written in MCM's format,
and opens it in the user's default text editing program if there is. """
# Check if the current tab has a mod written in MCM's format
currentTab = self.getCurrentTab()
for mod in currentTab.winfo_children()[0].mods:
if not mod.isAmfs:
webbrowser.open( mod.path )
break
else:
msg( "No text file mods (those written in MCM's standard format) were found in this tab. "
"These appear to be in AMFS format (ASM Mod Folder Structure), "
"which should open to a folder.", 'No MCM Style Mods Found', globalData.gui.root )
def openLibraryFolder( self ):
openFolder( self.libraryFolder )
def exportDOL( self ):
if globalData.disc:
exportSingleFileWithGui( globalData.disc.dol )
else:
msg( 'No disc has been loaded!' )
def saveGctFile( self ):
""" Simple wrapper for the 'Save GCT' button. Creates a Gecko Code Type file
using a tweak of the function used for creating INI files. """
self.saveIniFile( createForGCT=True )
def saveIniFile( self, createForGCT=False ):
# Check whether there are any mods selected
for mod in globalData.codeMods:
if mod.state == 'enabled' or mod.state == 'pendingEnable': break
else: # The loop above didn't break, meaning there are none selected
msg( 'No mods are selected!' )
return
# Decide on a default file name for the GCT file
if globalData.disc and globalData.disc.gameId:
initialFilename = globalData.disc.gameId
else:
initialFilename = 'Codes'
# Set the file type & description
if createForGCT:
fileExt = '.gct'
fileTypeDescription = "Gecko Code Type files"
else:
fileExt = '.ini'
fileTypeDescription = "Code Initialization files"
# Get a save filepath from the user
targetFile = tkFileDialog.asksaveasfilename(
title="Where would you like to save the {} file?".format( fileExt[1:].upper() ),
initialdir=globalData.getLastUsedDir(),
initialfile=initialFilename,
defaultextension=fileExt,
filetypes=[ (fileTypeDescription, fileExt), ("All files", "*") ]
)
if targetFile == '':
return # No filepath; user canceled
# Remember current settings
targetFileDir = os.path.split( targetFile )[0].encode('utf-8').strip()
globalData.setLastUsedDir( targetFileDir )
# Get the revision desired for this codeset
if globalData.disc:
dolRevision = globalData.disc.dol.revision
else: # Not yet known; prompt the user for it
revisionWindow = RevisionPromptWindow( 'Choose the region and game version that this codeset is for:', 'NTSC', '02' )
# Check the values gained from the user prompt (empty strings mean they closed or canceled the window)
if not revisionWindow.region or not revisionWindow.version:
return
dolRevision = revisionWindow.region + ' ' + revisionWindow.version
# Load the DOL for this revision (if one is not already loaded).
# This may be needed for formatting the code, in order to calculate RAM addresses from DOL offsets
try:
vanillaDol = globalData.getVanillaDol()
except Exception as err:
printStatus( 'Unable to create the {} file; {}'.format(fileExt[1:].upper(), err.message) )
return False
#if vanillaDol.revision != dolRevision: # todo
# Get and format the individual mods
geckoFormattedMods = []
missingTargetRevision = []
containsSpecialSyntax = []
saveString = 'Saved to ' + fileExt[1:].upper()
# Collect Gecko code strings and update the mod/modModule states
for tab in self.getAllTabs():
for mod in tab.winfo_children()[0].mods:
if mod.state == 'enabled' or mod.state == 'pendingEnable':
if dolRevision in mod.data:
geckoCodeString = mod.buildGeckoString( vanillaDol, createForGCT )
if geckoCodeString == '':
containsSpecialSyntax.append( mod.name )
else:
geckoFormattedMods.append( geckoCodeString )
# Update the mod's status (appearance) so the user knows what was saved
mod.setState( 'enabled', saveString, updateControlPanelCounts=False )
else:
missingTargetRevision.append( mod.name )
self.updateInstalledModsTabLabel()
# Save the text string to a GCT/INI file if any mods were able to be formatted
if geckoFormattedMods:
if createForGCT:
# Save the hex code string to the file as bytes
hexString = '00D0C0DE00D0C0DE' + ''.join( geckoFormattedMods ) + 'F000000000000000'
with open( targetFile, 'wb' ) as newFile:
newFile.write( bytearray.fromhex(hexString) )
else:
# Save as human-readable text
with open( targetFile, 'w' ) as newFile:
newFile.write( '\n\n'.join(geckoFormattedMods) )
printStatus( fileExt[1:].upper() + ' file created' )
# Notify the user of any codes that could not be included
warningMessage = ''
if missingTargetRevision:
warningMessage = ( "The following mods could not be included because they do not contain "
"code changes for the DOL revision you've selected:\n\n" + '\n'.join(missingTargetRevision) )
if containsSpecialSyntax:
warningMessage += ( "\n\nThe following mods could not be included because they contain special syntax (such as Standalone Functions or "
"RAM symbols) which are not currently supported in " + fileExt[1:].upper() + " file creation:\n\n" + '\n'.join(containsSpecialSyntax) )
if warningMessage:
cmsg( warningMessage.lstrip(), 'Warning' )
globalData.gui.playSound( 'menuChange' )
def saveCodeLibraryAs( self ):
""" Re-save all mods in the library in the desired format. """
# Ensure there's something to be saved
if not globalData.codeMods:
msg( 'There are no codes loaded to save!' )
return
# Prompt the user to determine what kind of format to use
userPrompt = PromptHowToSaveLibrary()
formatChoice = userPrompt.typeVar.get()
if formatChoice == -1: return # User canceled
# Ask for a folder to save the new library to
libraryPath = globalData.getModsFolderPath()
targetFolder = tkFileDialog.askdirectory(
title="Choose where to save this library.",
initialdir=libraryPath
)
if not targetFolder: return # User canceled
failedSaves = []
if formatChoice == 0: # Mini
msg( 'Not yet implemented; lmk if you want to use this.' )
elif formatChoice == 1: # MCM
msg( 'Not yet implemented; lmk if you want to use this.' )
else: # AMFS
# Try to get a vanilla DOL for address validation, and attempt to convert the mod
try:
vanillaDol = globalData.getVanillaDol()
except Exception as err:
printStatus( 'Unable to load a vanilla DOL; {}'.format(err.message) )
vanillaDol = None
for mod in globalData.codeMods:
try:
# Remove the filename component from mini/MCM paths, and add a new folder name component
if not mod.isAmfs:
# Get the path of this mod, relative to the library root folder
dirname = os.path.dirname( mod.path )
relPath = os.path.relpath( dirname, libraryPath )
# Use the relative path to build a new path within the new target library folder
newFolder = removeIllegalCharacters( mod.name )
newPath = os.path.join( targetFolder, relPath, newFolder )
else:
relPath = os.path.relpath( mod.path, libraryPath )
newPath = os.path.join( targetFolder, relPath )
# Attempt to convert Gecko codes to standard static overwrites and injections
if mod.type == 'gecko':
convertedGeckoMod = self.convertGeckoCode( mod, vanillaDol )
if convertedGeckoMod:
mod = convertedGeckoMod # Save this in AMFS
# Fall back to saving in MCM-Gecko format if the above conversion failed
else:
# Construct a new filepath for the mod using the new library folder path
filename = os.path.basename( mod.path )
newPath = os.path.join( targetFolder, relPath, filename )
mod.path = os.path.normpath( newPath )
mod.fileIndex = -1 # Trigger the following method to append the mod to the end of the file
# Save in the MCM-Gecko format
success = mod.saveInMcmFormat( showErrors=False )
if not success:
failedSaves.append( mod.name )
continue
# Save the mod
mod.path = os.path.normpath( newPath )
success = mod.saveInAmfsFormat()
if not success:
print( 'Unable to save {} in AMFS format'.format(mod.name) )
failedSaves.append( mod.name )
except Exception as err:
print( 'Unable to save {} in AMFS format; {}'.format(mod.name, err) )
failedSaves.append( mod.name )
globalData.gui.updateProgramStatus( 'Library save complete', success=True )
if failedSaves:
cmsg( 'These mods could not be saved (you may want to try saving these individually to identify specific problems):\n\n' + ', '.join(failedSaves), 'Failed Saves' )
def selectAllMods( self, event ):
currentTab = self.getCurrentTab()
for module in self.getModModules( currentTab ):
if module.mod.state == 'pendingDisable': module.setState( 'enabled', updateControlPanelCounts=False )
elif module.mod.state == 'disabled': module.setState( 'pendingEnable', updateControlPanelCounts=False )
self.updateInstalledModsTabLabel( currentTab )
globalData.gui.playSound( 'menuChange' )
def deselectAllMods( self, event ):
currentTab = self.getCurrentTab()
for module in self.getModModules( currentTab ):
if module.mod.state == 'pendingEnable': module.setState( 'disabled', updateControlPanelCounts=False )
elif module.mod.state == 'enabled': module.setState( 'pendingDisable', updateControlPanelCounts=False )
self.updateInstalledModsTabLabel( currentTab )
globalData.gui.playSound( 'menuChange' )
def selectWholeLibrary( self, event ):
for tab in self.getAllTabs():
guiModules = self.getModModules( tab )
if guiModules:
for module in guiModules:
mod = module.mod
if mod.state == 'pendingDisable': module.setState( 'enabled', updateControlPanelCounts=False )
elif mod.state == 'disabled': module.setState( 'pendingEnable', updateControlPanelCounts=False )
else:
# Update the internal mod references
for mod in tab.winfo_children()[0].mods:
if mod.state == 'pendingDisable': mod.state = 'enabled'
elif mod.state == 'disabled': mod.state = 'pendingEnable'
self.updateInstalledModsTabLabel()
globalData.gui.playSound( 'menuChange' )
def deselectWholeLibrary( self, event ):
for tab in self.getAllTabs():
guiModules = self.getModModules( tab )
if guiModules:
for module in guiModules:
mod = module.mod
if mod.state == 'pendingEnable': module.setState( 'disabled', updateControlPanelCounts=False )
elif mod.state == 'enabled': module.setState( 'pendingDisable', updateControlPanelCounts=False )
else:
# Update the internal mod references
for mod in tab.winfo_children()[0].mods:
if mod.state == 'pendingEnable': mod.state = 'disabled'
elif mod.state == 'enabled': mod.state = 'pendingDisable'
self.updateInstalledModsTabLabel()
globalData.gui.playSound( 'menuChange' )
def convertGeckoCode( self, mod, dol=None ):
""" Attempts to convert the given Gecko CodeMod object to one consisting of only static overwrites and injections.
Returns None if unsuccessful. """
try:
# Create a copy of the mod (this deep-copy should include basic properties, includePaths, webLinks, etc.)
modModule = mod.guiModule
mod.guiModule = None # Need to detatch for deepcopy; reattach after successful conversion
newMod = copy.deepcopy( mod )
newMod.name = mod.name + ' (Converted)'
newMod.data = {}
newMod.path = ''
newMod.type = 'static'
for revision, changes in mod.data.items():
for codeChange in changes:
if codeChange.type == 'gecko':
# Prepend an artificial title for the parser and parse it
customCode = codeChange.rawCode.splitlines()
customCode.insert( 0, '$TitlePlaceholder' )
codeChangeTuples = self.parser.parseGeckoCode( customCode )[-1]
if not codeChangeTuples:
raise Exception( 'Unable to parse code changes for Gecko code' )
# Add new code change modules
newMod.setCurrentRevision( revision )
for changeType, address, _, customCodeLines, annotation in codeChangeTuples:
# Validate the address
if dol:
errorMsg = dol.normalizeDolOffset( address )[1]
if errorMsg:
problemMessage = ( 'A problem was detected with an address, {}, '
'for the mod "{}";{}'.format(address, mod.name, errorMsg.split(';')[1]) )
raise Exception( problemMessage )
# Create a new CodeChange object and attach it to the internal mod module
if changeType == 'static':
codeChange = newMod.addStaticOverwrite( address, customCodeLines, '', annotation=annotation )
elif changeType == 'injection':
codeChange = newMod.addInjection( address, customCodeLines, '', annotation=annotation )
else:
raise Exception( 'Invalid code change type from Gecko code parser:', changeType )
else:
newMod.data[revision].append( codeChange )
newMod.guiModule = modModule
newMod.setCurrentRevision( mod.currentRevision )
return newMod
except Exception as err:
print( err )
return None
def summarizeChanges( self ):
""" Used for tracking codes pending installation or uninstallation. """
lines = []
modsToInstall = 0
modsToUninstall = 0