forked from DRGN-DRC/Melee-Code-Manager
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Melee Code Manager.py
9829 lines (7550 loc) · 444 KB
/
Melee Code Manager.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 Python file uses the following encoding: utf-8
# ------------------------------------------------------------------- #
# ~ ~ Written by DRGN of SmashBoards (Daniel R. Cappel); June, 2015 ~ ~ #
# - - [Python v2.7.9 - 2.7.16] - - #
# ------------------------------------------------------------------- #
programVersion = '4.4.2'
# # Find the official thread here:
# http://smashboards.com/threads/melee-code-manager-easily-add-codes-to-your-game.416437/
# Primary logic.
import os # For various file and directory operations.
import time # For performance testing
import json # For opening/parsing yaml config files (used for alternate mod folder structure syntax)
import errno
import codecs
import sys, csv
import webbrowser # Used to open a web page (the Melee Workshop, from the createNewDolMod function) or text file.
import subprocess, math # For communication with command line, and rounding operations, respectively.
import struct, binascii # For converting byte strings to integers. And binascii for the file hash function.
from decimal import Decimal # For the Code Creation tab's number conversions
from string import hexdigits # For checking that a string only consists of hexadecimal characters.
from binascii import hexlify
from collections import OrderedDict
from sets import Set # Used for duplicate mod detection and other unique lists
# GUI stuff
from Tkinter import Tk, Toplevel, Frame, StringVar, IntVar, BooleanVar, Label, OptionMenu, Spinbox, Button, Menu, Scrollbar, Canvas
from commonGuiModules import getWindowGeometry, basicWindow, CopyableMsg, PopupEntryWindow, PopupScrolledTextWindow, ToolTip
import Tkinter, ttk, tkMessageBox, tkFileDialog, tkFont
from ScrolledText import ScrolledText
from PIL import Image, ImageTk # For working with more image formats than just GIF.
from urlparse import urlparse # For validating and security checking URLs
from threading import Thread
from newTkDnD import TkDND # Access files given (drag-and-dropped) onto the running program GUI.
import pyaudio, wave
import webbrowser # Used for opening web pages
# User defined settings / persistent memory.
import settings as settingsFile
import ConfigParser
settings = ConfigParser.SafeConfigParser()
settings.optionxform = str # Tells the parser to preserve case sensitivity (for camelCase).
# Set up some useful globals
scriptHomeFolder = os.path.abspath( os.path.dirname(sys.argv[0]) )
dolsFolder = scriptHomeFolder + '\\Original DOLs'
imageBank = {} # Populates with key=fileNameWithoutExt, value=ImageTk.PhotoImage object
soundBank = {} # Populates with key=fileNameWithoutExt, value=fullFilePath
genGlobals = { # General Globals (migrate other globals to here)
'optionsFilePath': scriptHomeFolder + '\\options.ini',
'allModNames': Set(), # Will contain only unique mod names (used for duplicate mod detection)
'allMods': [],
'allStandaloneFunctions': {},
'originalDolRevisions': [],
'modifiedRegions': []
}
# The following are saved in the options.ini file (if it exists). To access them within the program, use:
# settings.get( 'General Settings', [valueName] )
# settings.set( 'General Settings', [valueName], [newValue] )
# And save them to file with a call to saveOptions()
generalOptionDefaults = {
'modsFolderPath': scriptHomeFolder + '\\Mods Library',
'modsFolderIndex': '0',
'defaultSearchDirectory': os.path.expanduser( '~' ),
'defaultFileFormat': 'iso',
'onlyUpdateGameSettings': 'False',
'hexEditorPath': '',
# 'altFontColor': '#d1cede', # A shade of silver; useful for high-contrast system themes
'offsetView': 'ramAddress', # alternate acceptable value=dolOffset (not case sensitive or affected by spaces)
'summaryOffsetView': 'dolOffset',
'sortSummaryByOffset': 'False'
}
overwriteOptions = OrderedDict() # Populates with key=customCodeRegionName, value=BooleanVar (to describe whether the regions should be used.)
# The following two dictionaries are only used for SSBM, for the game's default settings.
# Key = target widget, value = tuple of "(tableOffset, gameDefault, tourneyDefault [, str translations])".
# The gameDefault and tourneyDefault integers are indexes, relating to the string portion of the tuple (if it has a string portion).
# If the tuple doesn't have a string portion, then the values are instead the direct value for the setting (e.g. 3 stock or 4 stock).
settingsTableOffset = { 'NTSC 1.00': 0x3CFB90, 'NTSC 1.01': 0x3D0D68, 'NTSC 1.02': 0x3D1A48, 'NTSC 1.03': 0x3D1A48, 'PAL 1.00': 0x3D20C0 }
gameSettingsTable = {
'gameModeSetting': (2, 0, 1, 'Time', 'Stock', 'Coin', 'Bonus'), # AA
'gameTimeSetting': (3, 2, 2), # BB
'stockCountSetting': (4, 3, 4), # CC
'handicapSetting': (5, 0, 0, 'Off', 'Auto', 'On'), # DD
'damageRatioSetting': (6, 1.0, 1.0), # EE
'stageSelectionSetting': (7, 0, 0, 'On', 'Random', 'Ordered', 'Turns', 'Loser'), # FF
'stockTimeSetting': (8, 0, 8), # GG
'friendlyFireSetting': (9, 0, 1, 'Off', 'On'), # HH
'pauseSetting': (10, 1, 0, 'Off', 'On'), # II
'scoreDisplaySetting': (11, 0, 1, 'Off', 'On'), # JJ
'selfDestructsSetting': (12, 0, 0, '-1', '0', '-2'), # KK
'itemFrequencySetting': (24, 3, 0, 'None', 'Very Low', 'Low',
'Medium', 'High', 'Very High', 'Extremely High'), # PP
'itemToggleSetting': (36, 'FFFFFFFF', '00000000'),
'p1RumbleSetting': (40, 1, 0, 'Off', 'On'), # R1
'p2RumbleSetting': (41, 1, 0, 'Off', 'On'), # R2
'p3RumbleSetting': (42, 1, 0, 'Off', 'On'), # R3
'p4RumbleSetting': (43, 1, 0, 'Off', 'On'), # R4
#'soundBalanceSetting': (44, 0, 0), # MM
#'deflickerSetting': (45, 1, 1, 'Off', 'On'), # SS
#'languageSetting': (46, ), # Game ignores this in favor of system default? # LL
'stageToggleSetting': (48, 'FFFFFFFF', 'E70000B0') # TT
#'bootToSetting': ()
#'dbLevelSetting': ()
} # The variables that the program and GUI use to track the user's changes to these settings are contained in 'currentGameSettingsValues'
#=========================#
# ~ ~ General Functions ~ ~ #
#=========================#
def msg( *args ):
if len(args) > 2: tkMessageBox.showinfo( message=args[0], title=args[1], parent=args[2] )
elif len(args) > 1: tkMessageBox.showinfo( message=args[0], title=args[1] )
else: tkMessageBox.showinfo( message=args[0] )
def toInt( input ): # Converts 1, 2, and 4 byte bytearray values to integers.
try:
byteLength = len( input )
if byteLength == 1: return struct.unpack( '>B', input )[0] # big-endian unsigned char (1 byte)
elif byteLength == 2: return struct.unpack( '>H', input )[0] # big-endian unsigned short (2 bytes)
else: return struct.unpack( '>I', input )[0] # big-endian unsigned int (4 bytes)
except:
raise ValueError( 'toInt was not able to convert the ' + str(type(input))+' type' )
def isNaN( var ): ## Test if a variable 'is Not a Number'
try:
float( var )
return False
except ValueError:
return True
def isEven( inputVal ):
if inputVal % 2 == 0: return True # Non-zero modulus indicates an odd number.
else: return False
def roundTo32( x, base=32 ): # Rounds up to nearest increment of 32.
return int( base * math.ceil(float(x) / base) )
def uHex( integer ): # Quick conversion to have a hex function which uses uppercase characters.
if integer == 0: return '0'
else: return '0x' + hex( integer )[2:].upper()
def toHex( intOrStr, padTo ): # Creates a hex string (from an int or int string) and pads the result to n zeros (nibbles), the second parameter.
return "{0:0{1}X}".format( int( intOrStr ), padTo )
def grammarfyList( theList ): # For example, the list [apple, pear, banana, melon] becomes the string 'apple, pear, banana, and melon'.
if len(theList) == 1: return str(theList[0])
elif len(theList) == 2: return str(theList[0]) + ' and ' + str(theList[1])
else:
string = ', '.join( theList )
indexOfLastComma = string.rfind(',')
return string[:indexOfLastComma] + ', and ' + string[indexOfLastComma + 2:]
def convertCamelCase( originalString ): # Normalizes camelCase strings to normal writing; e.g. "thisIsCamelCase" to "This is camel case"
stringList = []
for character in originalString:
if stringList == []: stringList.append( character.upper() ) # Capitalize the first character in the string
elif character.isupper(): stringList.append( ' ' + character )
else: stringList.append( character )
return ''.join( stringList )
def humansize( nbytes ): # Used for converting file sizes, in terms of human readability.
suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
if nbytes == 0: return '0 B'
i = 0
while nbytes >= 1024 and i < len(suffixes)-1:
nbytes /= 1024.
i += 1
f = ('%.2f' % nbytes).rstrip('0').rstrip('.')
return '%s %s' % (f, suffixes[i])
def createFolders( folderPath ):
try:
os.makedirs( folderPath )
except OSError as exc: # Suppresses error if the directory already exists. Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir( folderPath ):
pass
else: raise
def findAll( stringToLookIn, subString, charIncrement=2 ): # Finds ALL instances of a string in another string
matches = []
i = stringToLookIn.find( subString )
while i >= 0:
matches.append( i )
i = stringToLookIn.find( subString, i + charIncrement ) # Change 2 to 1 if not going by bytes.
return matches
def getType( object ):
return str(type(object)).replace("<type '", '').replace("'>", '')
# def CRC32_from_file( filepath ):
# #buf = open( filepath, 'rb').read()
# with open( filepath, 'rb' ) as file: buf = file.read()
# buf = (binascii.crc32( buf ) & 0xFFFFFFFF)
# return "%08X" % buf
def validHex( offset ): # Accepts a string.
if offset == '': return False
return all(char in hexdigits for char in offset) # Returns Boolean
def copyToClipboard( text ):
root.clipboard_clear()
root.clipboard_append( text )
def rgb2hex( color ): return '#{:02x}{:02x}{:02x}'.format( color[0], color[1], color[2]) # input can be RGBA, but output will still be RGB
def hex2rgb( hexColor ): # Expects '#RRGGBB' or '#RRGGBBAA'
hexColor = hexColor.replace( '#', '' )
channelsList = []
if len( hexColor ) % 2 != 0: # Checks whether the string is an odd number of characters
raise ValueError( 'Input to hex2rgb must be an even number of characters!' )
else:
for i in xrange( 0, len(hexColor), 2 ): # Iterate by 2 over the length of the input string
channelByte = hexColor[i:i+2]
channelsList.append( int( channelByte, 16 ) )
return tuple(channelsList)
def name2rgb( name ):
""" Converts a Tkinter color name to an RGB tuple. """
colorChannels = root.winfo_rgb( name ) # Returns a 16-bit RGB tuple (0-65535)
return tuple( [channel / 256 for channel in colorChannels] ) # Converts to an 8-bit RGB tuple (0-255)
def openFolder( folderPath, fileToSelect='' ): # Slow function, but cannot select files
folderPath = os.path.abspath( folderPath ) # Turns relative to absolute paths, and normalizes them (switches / for \, etc.)
if os.path.exists( folderPath ):
if fileToSelect: # Slow method, but can select/highlight items in the folder
if not os.path.exists( folderPath + '\\' + fileToSelect ):
print( 'Could not find this file: \n\n' + fileToSelect )
return
command = '"C:\\Windows\\explorer.exe" /select, \"{}\\{}\"'.format( folderPath, fileToSelect )
try:
outputStream = subprocess.check_output(command, shell=False, stderr=subprocess.STDOUT, creationflags=0x08000000) # shell=True gives access to all shell features.
except subprocess.CalledProcessError as error:
outputStream = str(error.output)
if len(outputStream) != 0:
exitCode = str(error.returncode)
print( 'IPC error: \n\n' + outputStream + '\n\nErrorlevel ' + exitCode )
except Exception as generalError:
print( 'IPC error: \n\n' + generalError )
else: # Fast method, but cannot select files
os.startfile( folderPath )
else: print( 'Could not find this folder: \n\n' + folderPath )
#=====================================#
# ~ ~ Initialization & File Reading ~ ~ #
#=====================================#
class dolInitializer( object ):
def __init__( self ):
self.reset()
def reset( self ):
self.path = ''
self.type = '' # Essentially the file type/extension. Expected to be 'dol', 'iso', or 'gcm'
self.gameId = ''
self.discVersion = ''
self.region = ''
self.offset = 0
self.version = ''
self.revision = ''
self.isMelee = False # Will only be true for Super Smash Bros. Melee
self.is20XX = False
self.data = '' # Future #todo: upgrade to a bytearray, below
#self.bytes = bytearray()
self.sectionInfo = OrderedDict()
self.maxDolOffset = 0
self.maxRamAddress = 0
self.customCodeRegions = OrderedDict()
self.isLoading = False
self.project = -1
self.major = -1
self.minor = -1
self.patch = -1
def checkIfMelee( self ):
# Check the DOL for a string of "Super Smash Bros. Melee" at specific locations
self.isMelee = True
ssbmStringBytes = bytearray()
ssbmStringBytes.extend( "Super Smash Bros. Melee" )
dataBytes = bytearray.fromhex( self.data )
if dataBytes[0x3B78FB:0x3B7912] == ssbmStringBytes: self.region = 'NTSC'; self.version = '1.02' # most common, so checking for it first
elif dataBytes[0x3B6C1B:0x3B6C32] == ssbmStringBytes: self.region = 'NTSC'; self.version = '1.01'
elif dataBytes[0x3B5A3B:0x3B5A52] == ssbmStringBytes: self.region = 'NTSC'; self.version = '1.00'
elif dataBytes[0x3B75E3:0x3B75FA] == ssbmStringBytes: self.region = 'PAL'; self.version = '1.00'
else:
self.region = self.version = ''
self.isMelee = False
def getDolVersion( self ):
# The range 0xE4 to 0x100 in the DOL is normally unused padding. This can be used to specify a DOL version.
customDolVersionRange = bytearray.fromhex( self.data )[0xE4:0x100]
customVersionString = customDolVersionRange.split(b'\x00')[0].decode( 'ascii' )
# If a custom string exists, validate and use that, or else prompt the user (using disc region/version for predictors)
if customVersionString and ' ' in customVersionString: # Highest priority for determining version
apparentRegion, apparentVersion = normalizeRegionString( customVersionString ).split() # Should never return 'ALL' in this case
if ( apparentRegion == 'NTSC' or apparentRegion == 'PAL' ) and apparentVersion.find( '.' ) != -1:
self.region, self.version = apparentRegion, apparentVersion
if not self.region or not self.version:
# Check the filepath; if this is an original DOL in the Original DOLs folder, use the file name to determine version
if self.type == 'dol' and os.path.dirname( self.path ) == dolsFolder:
self.region, self.version = normalizeRegionString( os.path.basename(self.path) ).split()
return
# Attempt to predict details based on the disc, if present
regionSuggestion = 'NTSC'
versionSuggestion = '02'
if self.type == 'iso' or self.type == 'gcm':
if self.region == 'NTSC' or self.region == 'PAL':
regionSuggestion = self.region
if '.' in self.discVersion:
versionSuggestion = self.discVersion.split('.')[1]
userMessage = ( "The revision of the DOL within this disc is being predicted from the disc's details. Please verify them below. "
"(If this disc has not been altered, these predictions can be trusted.)" )
else:
userMessage = "This DOL's revision could not be determined. Please select a region and game version below."
userMessage += ' Note that codes may not be able to be installed or detected properly if these are set incorrectly.'
revisionWindow = RevisionPromptWindow( userMessage, regionSuggestion, versionSuggestion )
if revisionWindow.region and revisionWindow.version: # Save the user-confirmed revision
self.region = revisionWindow.region
self.version = revisionWindow.version
self.writeSig()
# Write the new DOL data to file/disc
if self.type == 'dol':
with open( self.path, 'wb') as dolBinary:
dolBinary.write( bytearray.fromhex(self.data) )
elif ( self.type == 'iso' or self.type == 'gcm' ) and self.offset != 0:
with open( self.path, 'r+b') as isoBinary:
isoBinary.seek( self.offset )
isoBinary.write( bytearray.fromhex(self.data) )
else: # Checking for the window sinceAssume the suggested revision
self.region = regionSuggestion
self.version = '1.' + versionSuggestion
msg( 'Without confirmation, the DOL file will be assumed as ' + self.region + ' ' + self.version + '. '
'If this is incorrect, you may run into problems detecting currently installed mods or with adding/removing mods. '
'And installing mods may break game functionality.', 'Revision Uncertainty', root )
def checkIf20XX( self ):
""" 20XX has a version string in the DOL at 0x0x3F7158, preceded by an ASCII string of '20XX'.
Versions up to 4.07++ used an ASCII string for the version as well (v4.07/4.07+/4.07++ have the same string).
Versions 5.x.x use a new method of project code, major version, minor version, and patch, respectively (one byte each). """
# Check for the '20XX' string
if self.data[0x3F7154*2:0x3F7154*2+8] != '32305858':
self.is20XX = ''
return
versionData = bytearray.fromhex( self.data[0x3F7158*2:0x3F7158*2+8] )
# Check if using the new v5+ format
if versionData[0] == 0:
self.project, self.major, self.minor, self.patch = struct.unpack( 'BBBB', versionData )
self.is20XX = '{}.{}.{}'.format( self.major, self.minor, self.patch )
else: # Using the old string format, with just major and minor
self.is20XX = versionData.decode( 'ascii' )
# Parse out major/minor versions
major, minor = self.is20XX.split( '.' )
self.major = int( major )
self.minor = int( minor )
# May be able to be more accurate
if self.is20XX == '4.07' and len( self.data ) == 0x438800 and bytearray.fromhex( self.data[-8:] ) == bytearray( b'\x00\x43\x88\x00' ):
self.is20XX = '4.07++'
self.patch = 2
else:
self.patch = 0
def writeSig( self ):
""" Saves the DOL's determined revision to the file in an unused area,
so that subsequent loads don't have to ask about it. """
# Create the hex string; [DOL Revision]00[Program Version]
revisionDataHex = ( self.region + ' ' + self.version ).encode("hex")
revisionDataHex += '00' + ( 'MCM v' + programVersion ).encode("hex") # Adds a stop byte and MCM's version number
# Ensure the data length never exceeds 0x1C bytes
nibLength = len( revisionDataHex )
if nibLength > 0x38: # 0x38 = 0x1C * 2
revisionDataHex = revisionDataHex[:0x38]
padding = ''
else:
# Fill the rest of the section with zeroes
padding = '0' * ( 0x38 - nibLength )
# Write the string to the file data
self.data = self.data[:0x1C8] + revisionDataHex + padding + self.data[0x200:] # Static values doubled to count by nibbles; 0x1C8 = 0xE4 * 2
def getDolDataFromIso( self, isoPath ):
with open( isoPath, 'rb') as isoBinary:
# Collect info on the disc
self.gameId = isoBinary.read(6).decode( 'ascii' ) # First 6 bytes
isoBinary.seek( 1, 1 ) # Second arg means to seek relative to current position
versionHex = isoBinary.read(1).encode("hex")
regionCode = self.gameId[3]
ntscRegions = ( 'A', 'E', 'J', 'K', 'R', 'W' )
if regionCode in ntscRegions: self.region = 'NTSC'
else: self.region = 'PAL'
self.discVersion = '1.' + versionHex
# Get the DOL's ISO file offset, length, and data.
isoBinary.seek( 0x0420 )
self.offset = toInt( isoBinary.read(4) ) # Should be 0x1E800 for SSBM NTSC v1.02
tocOffset = toInt( isoBinary.read(4) )
dolLength = tocOffset - self.offset # Should be 0x438600 for SSBM NTSC v1.02
isoBinary.seek( self.offset )
self.data = isoBinary.read( dolLength ).encode("hex")
def load( self, filepath ):
self.reset()
self.isLoading = True
# Initialize the file path and type
self.path = filepath
self.type = os.path.splitext( filepath )[1].lower().replace( '.', '' )
# Validate the given path
if not filepath or not os.path.exists( filepath ):
msg( 'The recieved filepath was invalid, or for some reason the file was not found.' )
return
# Get the DOL's file data
if self.type == 'iso' or self.type == 'gcm':
self.getDolDataFromIso( filepath )
elif self.type == 'dol':
with open( filepath, 'rb') as binaryFile:
self.data = binaryFile.read().encode("hex")
else:
msg( "The given file doesn't appear to be an ISO/GCM or DOL file. \nIf it in fact is, "
"you'll need to rename it with a file extension of '.iso/.gcm' or '.dol'.", 'Incorrect file type.' )
return
# Check if this is a revision of Super Smash Bros. Melee for the Nintendo GameCube.
self.checkIfMelee()
# General check for DOL revision (region + version). This will prompt the user if it cannot be determined.
self.getDolVersion()
if self.isMelee:
self.checkIf20XX()
if ( self.region == 'NTSC' or self.region == 'PAL' ) and '.' in self.version:
self.revision = self.region + ' ' + self.version
self.parseHeader()
self.isLoading = False
def parseHeader( self ):
headerData = bytearray.fromhex( self.data )[:0x100]
# Separate the section information
textFileOffsets = headerData[:0x1C]
dataFileOffsets = headerData[0x1C:0x48]
textMemAddresses = headerData[0x48:0x64]
dataMemAddresses = headerData[0x64:0x90]
textSizes = headerData[0x90:0xAC]
dataSizes = headerData[0xAC:0xD8]
self.bssMemAddress = toInt( headerData[0xD8:0xDC] )
self.bssSize = toInt( headerData[0xDC:0xE0] )
self.entryPoint = toInt( headerData[0xE0:0xE4] )
self.maxDolOffset = 0
self.maxRamAddress = 0
# Combine data points into a single definition for each text section
for i in xrange( 6 ): # No more than 6 possible
listIndex = i * 4
fileOffset = toInt( textFileOffsets[listIndex:listIndex+4] )
memAddress = toInt( '\x00' + textMemAddresses[listIndex+1:listIndex+4] )
size = toInt( textSizes[listIndex:listIndex+4] )
# If any of the above values are 0, there are no more sections
if fileOffset == 0 or memAddress == 0 or size == 0: break
self.sectionInfo['text'+str(i)] = ( fileOffset, memAddress, size )
# Find the max possible dol offset and ram address for this game's dol
if fileOffset + size > self.maxDolOffset: self.maxDolOffset = fileOffset + size
if memAddress + size > self.maxRamAddress: self.maxRamAddress = memAddress + size
# Combine data points into a single definition for each data section
for i in xrange( 10 ): # No more than 10 possible
listIndex = i * 4
fileOffset = toInt( dataFileOffsets[listIndex:listIndex+4] )
memAddress = toInt( '\x00' + dataMemAddresses[listIndex+1:listIndex+4] )
size = toInt( dataSizes[listIndex:listIndex+4] )
if fileOffset == 0 or memAddress == 0 or size == 0: break
self.sectionInfo['data'+str(i)] = ( fileOffset, memAddress, size )
# Find the max possible dol offset and ram address for this game's dol
if fileOffset + size > self.maxDolOffset: self.maxDolOffset = fileOffset + size
if memAddress + size > self.maxRamAddress: self.maxRamAddress = memAddress + size
def loadCustomCodeRegions( self ):
""" Loads and validates the custom code regions available for this DOL revision.
Filters out regions pertaining to other revisions, and those that fail basic validation. """
incompatibleRegions = []
print '\nLoading custom code regions....'
# Load all regions applicable to this DOL (even if disabled in options)
for fullRegionName, regions in settingsFile.customCodeRegions.items():
revisionList, regionName = parseSettingsFileRegionName( fullRegionName )
# Skip recent 20XX regions if this is not a recent 20XX DOL (v4.07++ or higher)
# if not self.is20XX and regionName.startswith( '20XXHP' ):
# continue
# Check if the region/version of these regions are relavant to the currently loaded DOL revision
if 'ALL' in revisionList or self.revision in revisionList:
# Validate the regions; perform basic checks that they're valid ranges for this DOL
for i, ( regionStart, regionEnd ) in enumerate( regions, start=1 ):
# Check that the region start is actually smaller than the region end
if regionStart >= regionEnd:
msg( 'Warning! The starting offset for region ' + str(i) + ' of "' + regionName + '" for ' + self.revision + ' is greater or '
"equal to the ending offset. A region's starting offset must be smaller than the ending offset.", 'Invalid Custom Code Region' )
incompatibleRegions.append( regionName )
break
# Check that the region start is within the DOL's code or data sections
elif regionStart < 0x100 or regionStart >= self.maxDolOffset:
print "Region start (0x{:X}) for {} is outside of the DOL's code\\data sections.".format( regionStart, regionName )
incompatibleRegions.append( regionName )
break
# Check that the region end is within the DOL's code or data sections
elif regionEnd > self.maxDolOffset:
print "Region end (0x{:X}) for {} is outside of the DOL's code\\data sections.".format( regionEnd, regionName )
incompatibleRegions.append( regionName )
break
# Regions validated; allow them to show up in the GUI (Code-Space Options)
if regionName not in incompatibleRegions:
self.customCodeRegions[regionName] = regions
if incompatibleRegions:
print ( '\nThe following regions are incompatible with the ' + self.revision + ' DOL, '
'because one or both offsets fall outside of the offset range of the file:\n\n\t' + '\n\t'.join(incompatibleRegions) + '\n' )
class geckoInitializer( object ):
""" Collects and validates Gecko configuration settings from the
settings.py file and ensures Gecko codes can be used. """
def __init__( self ):
self.reset()
def reset( self ):
self.environmentSupported = False
self.hookOffset = -1
self.codelistRegion = ''
self.codelistRegionStart = self.codelistRegionEnd = -1
self.codehandlerRegion = ''
self.codehandlerRegionStart = self.codehandlerRegionEnd = -1
self.codehandler = bytearray()
self.codehandlerLength = 0
self.spaceForGeckoCodelist = 0
self.spaceForGeckoCodehandler = 0
#self.geckoConfigWarnings = [] # todo: better practice to use this to remember them until the user
# tries to enable Gecko codes (instead of messaging the user immediately)
# Check for a dictionary on Gecko configuration settings; this doesn't exist in pre-v4.0 settings files
self.geckoConfig = getattr( settingsFile, "geckoConfiguration", {} )
def checkSettings( self ):
self.reset()
if not dol.revision: return
self.setGeckoHookOffset()
if self.hookOffset == -1:
msg( 'Warning! No geckoConfiguration properties could be found in the settings.py file for DOL revision "' + dol.revision + '".'
'Gecko codes cannot be used until this is resolved.', 'Gecko Misconfiguration' )
return
self.codelistRegionStart, self.codelistRegionEnd = self.getCodelistRegion()
if self.codelistRegionStart == -1: return
self.codehandlerRegionStart, self.codehandlerRegionEnd = self.getCodehandlerRegion()
if self.codehandlerRegionStart == -1: return
self.codehandler = self.getGeckoCodehandler()
self.spaceForGeckoCodelist = self.codelistRegionEnd - self.codelistRegionStart
self.spaceForGeckoCodehandler = self.codehandlerRegionEnd - self.codehandlerRegionStart
# Check that the codehandler can fit in the space defined for it
if self.codehandlerLength > self.spaceForGeckoCodehandler:
msg( 'Warning! The region designated to store the Gecko codehandler is too small! The codehandler is ' + uHex( self.codehandlerLength ) + ' bytes '
'in size, while the region defined for it is ' + uHex( self.spaceForGeckoCodehandler) + ' bytes long (only the first section among those '
'regions will be used). Gecko codes cannot be used until this is resolved.', 'Gecko Misconfiguration' )
else:
# If this has been reached, everything seems to check out.
self.environmentSupported = True
# Set the maximum value for the gecko code fill meter
freeGeckoSpaceIndicator['maximum'] = self.spaceForGeckoCodelist
def setGeckoHookOffset( self ):
""" Checks for the geckoConfiguration dictionary in the config file, and gets the hook offset for the current revision. """
if not self.geckoConfig: # For backwards compatability with pre-v4.0 config file.
oldHooksDict = getattr( settingsFile, "geckoHookOffsets", {} )
if not oldHooksDict:
self.hookOffset = -1
else:
if dol.region == 'PAL':
self.hookOffset = oldHooksDict['PAL']
else: self.hookOffset = oldHooksDict[dol.version]
# The usual expectation for v4.0+ settings files
elif self.geckoConfig:
self.hookOffset = self.geckoConfig['hookOffsets'].get( dol.revision, -1 ) # Assigns -1 if this dol revision isn't found
def getCodelistRegion( self ):
# Get the region defined in settings.py that is to be used for the Gecko codelist
if not self.geckoConfig and dol.isMelee: # For backwards compatability with pre-v4.0 config file.
self.codelistRegion = 'DebugModeRegion'
elif self.geckoConfig:
self.codelistRegion = self.geckoConfig['codelistRegion']
# Check for the codelist region among the defined regions, and get its first DOL area
if self.codelistRegion in dol.customCodeRegions:
return dol.customCodeRegions[self.codelistRegion][0]
else:
msg( 'Warning! The region assigned for the Gecko codelist (under geckoConfiguration in the settings.py file) could not '
'be found among the code regions defined for ' + dol.revision + '. Double check the spelling, '
'and keep in mind that the strings are case-sensitive. Gecko codes cannot be used until this is resolved.', 'Gecko Misconfiguration' )
self.codelistRegion = ''
return ( -1, -1 )
def getCodehandlerRegion( self ):
# Get the region defined in settings.py that is to be used for the Gecko codehandler
if not self.geckoConfig and dol.isMelee: # For backwards compatability with pre-v4.0 config file.
self.codehandlerRegion = 'AuxCodeRegions'
elif self.geckoConfig:
self.codehandlerRegion = self.geckoConfig['codehandlerRegion']
# Check for the codehandler region among the defined regions, and get its first DOL area
if self.codehandlerRegion in dol.customCodeRegions:
return dol.customCodeRegions[self.codehandlerRegion][0]
else:
msg( 'Warning! The region assigned for the Gecko codehandler (under geckoConfiguration in the settings.py file) could not '
'be found among the code regions defined for ' + dol.revision + '. Double check the spelling, '
'and keep in mind that the strings are case-sensitive. Gecko codes cannot be used until this is resolved.', 'Gecko Misconfiguration' )
self.codehandlerRegion = ''
return ( -1, -1 )
def getGeckoCodehandler( self ):
# Get the Gecko codehandler from an existing .bin file if present, or from the settings.py file
if os.path.exists( scriptHomeFolder + '\\bin\\codehandler.bin' ):
with open( scriptHomeFolder + '\\bin\\codehandler.bin', 'rb' ) as binFile:
geckoCodehandler = bytearray( binFile.read() )
else:
geckoCodehandler = bytearray.fromhex( settingsFile.geckoCodehandler )
# Append any padding needed to align its length to 4 bytes (so that any codes applied after it will be aligned).
self.codehandlerLength = len( geckoCodehandler )
totalRequiredCodehandlerSpace = roundTo32( self.codehandlerLength, base=4 ) # Rounds up to closest multiple of 4 bytes
paddingLength = totalRequiredCodehandlerSpace - self.codehandlerLength # in bytes
padding = bytearray( paddingLength )
geckoCodehandler.extend( padding ) # Extend returns nothing
return geckoCodehandler
def loadGeneralOptions():
""" Check for user defined settings / persistent memory, from the "options.ini" file.
If values don't exist in it for particular settings, defaults are loaded from the 'generalOptionDefaults' dictionary. """
# Read the file if it exists (this should create it if it doesn't)
if os.path.exists( genGlobals['optionsFilePath'] ):
settings.read( genGlobals['optionsFilePath'] )
# Add the 'General Settings' section if it's not already present
if not settings.has_section( 'General Settings' ):
settings.add_section( 'General Settings' )
# Set default [hardcoded] settings only if their values don't exist in the options file; don't want to modify them if they're already set.
for key, option in generalOptionDefaults.items():
if not settings.has_option( 'General Settings', key ): settings.set( 'General Settings', key, option )
onlyUpdateGameSettings.set( settings.getboolean( 'General Settings', 'onlyUpdateGameSettings' ) ) # Sets a booleanVar for the GUI
def getModsFolderPath( getAll=False ):
pathsString = settings.get( 'General Settings', 'modsFolderPath' )
pathsList = csv.reader( [pathsString] ).next()
if getAll:
return pathsList
pathIndex = int( settings.get('General Settings', 'modsFolderIndex') )
if pathIndex < 0 or pathIndex >= len( pathsList ):
print 'Invalid mods library path index:', pathIndex
return pathsList[0]
return pathsList[pathIndex]
def loadRegionOverwriteOptions():
""" Checks saved options (the options.ini file) for whether or not custom code regions are selected for use (i.e. can be overwritten).
This is called just before scanning/parsing mod libraries, checking for enabled codes, or installing a mods list.
Creates new BooleanVars only on first run, which should exist for the life of the program (they will only be updated after that). """
# Check for options file / persistent memory.
if os.path.exists( genGlobals['optionsFilePath'] ): settings.read( genGlobals['optionsFilePath'] )
if not settings.has_section( 'Region Overwrite Settings' ): settings.add_section( 'Region Overwrite Settings' )
# Create BooleanVars for each defined region. These will be used to track option changes
for regionName in dol.customCodeRegions.keys():
# Create a new boolVar entry for this region.
if regionName not in overwriteOptions: # This function may have already been called (s)
overwriteOptions[ regionName ] = BooleanVar()
# If the options file contains an option for this region, use it.
if settings.has_option( 'Region Overwrite Settings', regionName ):
overwriteOptions[ regionName ].set( settings.getboolean( 'Region Overwrite Settings', regionName ) )
# Otherwise, set a default for this region's use.
else: overwriteOptions[ regionName ].set( False )
# Set the option for allowing Gecko codes, if it doesn't already exist (initial program load)
if 'EnableGeckoCodes' not in overwriteOptions:
overwriteOptions[ 'EnableGeckoCodes' ] = BooleanVar()
# First check whether a setting already exists for this in the options file
if settings.has_option( 'Region Overwrite Settings', 'EnableGeckoCodes' ):
allowGeckoCodes = settings.getboolean( 'Region Overwrite Settings', 'EnableGeckoCodes' )
else: # The option doesn't exist in the file
allowGeckoCodes = False
if not dol.data:
overwriteOptions[ 'EnableGeckoCodes' ].set( allowGeckoCodes )
return # No file loaded; can't confirm gecko settings are valid
# Check that the gecko configuration in settings.py are valid
if not allowGeckoCodes or not gecko.environmentSupported:
overwriteOptions[ 'EnableGeckoCodes' ].set( False )
else: # Make sure the appropriate regions required for gecko codes are also set.
if overwriteOptions[ gecko.codelistRegion ].get() and overwriteOptions[ gecko.codehandlerRegion ].get():
overwriteOptions[ 'EnableGeckoCodes' ].set( True )
else:
promptToUser = ( 'The option to enable Gecko codes is set, however the regions required for them, '
'the ' + gecko.codelistRegion + ' and ' + gecko.codehandlerRegion + ', are not set for use (i.e for partial or full overwriting).'
'\n\nDo you want to allow use of these regions \nin order to allow Gecko codes?' )
willUserAllowGecko( promptToUser, False, root ) # Will prompt the user with the above message and set the overwriteOption
def loadImageBank():
""" Loads and stores images required by the GUI. This allows all of the images to be
stored together in a similar manner, and ensures references to all of the loaded
images are stored, which prevents them from being garbage collected (which would
otherwise cause them to disappear from the GUI after rendering is complete). The
images are only loaded when first requested, and then kept for future reference. """
loadFailed = []
for item in os.listdir( os.path.join( scriptHomeFolder, 'imgs' ) ):
if not item.endswith( '.png' ): continue
filepath = os.path.join( scriptHomeFolder, 'imgs', item )
try:
imageBank[item[:-4]] = ImageTk.PhotoImage( Image.open(filepath) )
except: loadFailed.append( filepath )
if loadFailed:
msg( 'Unable to load some images:\n\n' + '\n'.join(loadFailed) )
def openFileByField( event ):
""" Called by the user pressing Enter in the "ISO / DOL" field. Simply attempts
to load whatever path is in the text field. """
readRecievedFile( openedFilePath.get().replace('"', '') )
playSound( 'menuChange' )
# Move the cursor position to the end of the field (default is the beginning)
event.widget.icursor( Tkinter.END )
def openFileByButton():
""" Called by the "Open" button. Prompts the user for a file to open, and then
attempts to open it. """
# Set the default file formats to choose from in the file chooser dialog box
defaultFileFormat = settings.get( 'General Settings', 'defaultFileFormat' )
if defaultFileFormat.lower() == 'dol' or defaultFileFormat.lower() == '.dol':
filetypes = [ ('Melee executable', '*.dol'), ('Disc image files', '*.iso *.gcm'), ('all files', '*.*') ]
else: filetypes = [ ('Disc image files', '*.iso *.gcm'), ('Melee executable', '*.dol'), ('all files', '*.*') ]
# Present a file chooser dialog box to the user
filepath = tkFileDialog.askopenfilename(
title="Choose a DOL or disc image file to open.",
initialdir=settings.get( 'General Settings', 'defaultSearchDirectory' ),
defaultextension=defaultFileFormat,
filetypes=filetypes
)
if filepath:
readRecievedFile( filepath )
playSound( 'menuChange' )
def restoreOriginalDol():
if problemWithDol(): return
elif not dol.revision:
msg( 'The revision of the currently loaded DOL could not be determined.' )
return
restoreConfirmed = tkMessageBox.askyesno( 'Restoration Confirmation', 'This will revert the currently loaded DOL to be practically '
'identical to a vanilla ' + dol.revision + ' DOL (loaded from the "Original DOLs" folder). '
'"Free space" regions selected for use will still be zeroed-out. This process does not preserve '
'a copy of the current DOL, and any current changes will be lost.\n\nAre you sure you want to do this?' )
if restoreConfirmed:
vanillaDol = loadVanillaDol() # Should prompt the user with details if there's a problem here
if vanillaDol and vanillaDol.data:
# Seems that the original DOL was loaded successfully. Perform the data replacement.
dol.data = vanillaDol.data # Will be re-signed when the user saves.
# Rescan for mods
checkForEnabledCodes()
# Ensure that the save buttons are enabled
saveChangesBtn.config( state='normal' )
saveChangesBtn2.config( state='normal' )
# Provide user feedback
playSound( 'menuChange' )
programStatus.set( 'Restoration Successful' )
else:
programStatus.set( 'Restoration Unsuccessful' )
def readRecievedFile( filepath, defaultProgramStatus='', checkForCodes=True ):
if dol.isLoading: # Simple failsafe
discVersion.set( '' )
dolVersion.set( 'File already loading!' )
return
# Reset/clear the gui.
discVersion.set( '' )
programStatus.set( defaultProgramStatus )
openedFilePath.set( '' )
dolVersion.set( 'Nothing Loaded' )
showRegionOptionsBtn.config( state='disabled' )
exportFileBtn.config( state='disabled' )
importFileBtn.config( state='disabled' )
restoreDolBtn.config( state='disabled' )
# Validate the given path, and update the default search directory and file format settings
normalizedPath = os.path.normpath( filepath ).replace('{', '').replace('}', '')
if not normalizedPath or not os.path.exists( normalizedPath ):
saveChangesBtn.config( state='disabled' )
saveChangesBtn2.config( state='disabled' )
saveChangesAsBtn.config( state='disabled' )
saveChangesAsBtn2.config( state='disabled' )
msg( 'Unable to find this file: "' + normalizedPath + '".', 'Invalid Filepath' )
return
else: # File seems good
saveChangesAsBtn.config( state='normal' )
saveChangesAsBtn2.config( state='normal' )
# The other standard save buttons will become enabled once changes are detected.
# Load the DOL file to be modded (from a disc or DOL file path)
dol.load( normalizedPath )
dol.loadCustomCodeRegions()
# Remember the given path and file type for later defaults
settings.set( 'General Settings', 'defaultSearchDirectory', os.path.dirname( normalizedPath ).encode('utf-8').strip() )
settings.set( 'General Settings', 'defaultFileFormat', os.path.splitext( normalizedPath )[1].replace('.', '').encode('utf-8').strip() )
saveOptions()
# Update the GUI with the ISO/DOL's details
if dol.revision: dolVersion.set( dol.version + ' DOL Detected' )
else: dolVersion.set( 'Unknown DOL Revision' )
openedFilePath.set( normalizedPath.replace('"', '') )
showRegionOptionsBtn.config( state='normal' )
if dol.type == 'iso' or dol.type == 'gcm':
if dol.discVersion: discVersion.set( dol.discVersion + ' Disc,' )
exportFileBtn.config( state='normal' )
elif dol.type == 'dol': importFileBtn.config( state='normal' )
restoreDolBtn.config( state='normal' )
# Enable buttons on the Summary tab
for widget in summaryTabFirstRow.rightColumnFrame.winfo_children():
widget['state'] = 'normal'
# Validate the settings.py file for parameters on the installation of Gecko codes
gecko.checkSettings()
# Output some info to console
if 1:
print '\nPath:', '\t', dol.path
print 'File Type:', '\t', dol.type
print 'Disc Version:', '\t', dol.discVersion
print 'DOL Offset:', '\t', hex( dol.offset )
print 'DOL Size:', '\t', hex( len(dol.data) /2 )
print 'DOL Region:', '\t', dol.region
print 'DOL Version:', '\t', dol.version
print 'Is Melee:', '\t', dol.isMelee
print 'Is 20XX: ', '\t', dol.is20XX
print 'Max DOL Offset:', hex( dol.maxDolOffset )
print 'bssMemAddress:', hex( dol.bssMemAddress ).replace('L', '')
print 'bssSize:', hex( dol.bssSize )
print 'entryPoint:', hex( dol.entryPoint ).replace('L', '')
if 1: # DOL Section printout
print '\n\tfO, RAM, size'
for sectionName, ( fileOffset, memAddress, size ) in dol.sectionInfo.items():
print sectionName + ':', hex(fileOffset), hex(memAddress), hex(size)
for regionName, regions in dol.customCodeRegions.items():
print '\n\t', regionName + ':\t', [(hex(start), hex(end)) for start, end in regions]
# Gecko configuration printout
print '\nGecko configuration:'
for key, value in gecko.__dict__.items():
if key == 'codehandler': continue
if type( value ) == int:
print '\t', key + ':', hex(value)
else:
print '\t', key + ':', value
print ''
# Load settings from the settings.py file
loadRegionOverwriteOptions()
twentyXXv4Option = overwriteOptions.get( "20XXHP 4.07 Regions" )
twentyXXv5Option = overwriteOptions.get( "20XXHP 5.0 Regions" )
# Prompt the user to ask if they'd like to enable 20XX code regions
if dol.is20XX and dol.major == 4 and dol.minor == 7 and twentyXXv4Option and not twentyXXv4Option.get(): # v4.07 loaded, but custom regions not enabled
if tkMessageBox.askyesno( 'Enable Custom Code Regions?', ('It Looks like the given ' + dol.type.upper() + ' is for 20XX, v4.07. '
'Would you like to enable the "20XXHP 4.07 Regions" for custom code?') ):
for regionName, boolVar in overwriteOptions.items():
if regionName == '20XXHP 4.07 Regions': boolVar.set( True )
else: boolVar.set( False )
saveOptions()