forked from DRGN-DRC/Melee-Modding-Wizard
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcodeMods.py
More file actions
3806 lines (2974 loc) · 149 KB
/
codeMods.py
File metadata and controls
3806 lines (2974 loc) · 149 KB
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 json
import struct
import codecs
import urlparse
import Tkinter as Tk
from string import hexdigits
from binascii import hexlify
from subprocess import Popen, PIPE
from collections import OrderedDict
# Internal Dependencies
import globalData
from basicFunctions import createFolders, removeIllegalCharacters, roundTo32, toHex, validHex, msg, printStatus, NoIndent, CodeModEncoder
from guiSubComponents import cmsg
ConfigurationTypes = { 'int8': 'b', 'uint8': 'B', 'mask8': 'B',
'int16': '>h', 'uint16': '>H', 'mask16': '>H',
'int32': '>i', 'uint32': '>I', 'mask32': '>I',
'float': '>f' }
class CodeChange( object ):
""" Represents a single code change to be made to the game, such
as a single code injection or static (in-place) overwrite. """
def __init__( self, mod, changeType, offset, origCode, rawCustomCode, annotation='', name='' ):
self.mod = mod
self._name = name # Filename for this code change (without extension)
self.type = changeType # String; one of 'static', 'injection', 'standalone', or 'gecko'
self.length = -1
self.offset = offset # String; may be a DOL offset or RAM address. Should be interpreted by one of the DOL normalization methods
self.isAssembly = False # Refers to the source/rawCode, not the preProcessedCode
self.isCached = False # Whether or not assembled hex code is available (may be a mixed .txt or binary .txt file)
self.syntaxInfo = [] # A list of lists. Each sub-list is of the form [ offset, length, syntaxType, codeLine, names ]
self._origCode = origCode
self._origCodePreprocessed = False
self.rawCode = rawCustomCode
self.preProcessedCode = ''
self.processStatus = -1
self.anno = annotation
@property
def origCode( self ):
""" Original code may have been provided with the defined mod (particularly, within the MCM format),
however it's not expected to be available from the AMFS format. This method will retrieve it from
a vanilla DOL if that is available. """
# If no original hexcode, try to get it from the vanilla disc
if not self._origCode:
if not self.offset:
return ''
# Get the DOL file
try:
dol = globalData.getVanillaDol()
except Exception as err:
printStatus( 'Unable to get DOL data; {}'.format(err.message), warning=True )
return ''
# Normalize the offset string, and get the target file data
dolOffset, error = dol.normalizeDolOffset( self.offset )
if error:
self.mod.parsingError = True
self.mod.errors.add( error )
return ''
# Determine the amount of code to get
if self.type == 'injection':
length = 4
else:
length = self.getLength()
if length == -1 or self.processStatus != 0:
return '' # Specific errors should have already been recorded
# Get the DOL data as a hex string
origData = dol.getData( dolOffset, length )
self._origCode = hexlify( origData ).upper()
self._origCodePreprocessed = True
# Pre-process the original code to remove comments and whitespace
if self._origCode and not self._origCodePreprocessed:
filteredLines = []
for line in self._origCode.splitlines():
line = line.split( '#' )[0].strip()
if not line: continue
filteredLines.append( ''.join(line.split()) ) # Removes whitespace from this line
filteredOriginal = ''.join( filteredLines )
# Validate the string to make sure it's only a hex string of the game's original code
if not validHex( filteredOriginal ):
msg( 'Problem detected while parsing "' + self.mod.name + '" in the mod library file "'
+ os.path.basename( self.mod.path ) + '" (index ' + str(self.mod.fileIndex+1) + '). '
'There is an invalid (non-hex) original hex value found:\n\n' + filteredOriginal, 'Incorrect Mod Formatting (Error Code 04.2)' )
self.mod.parsingError = True
self.mod.errors.add( 'Invalid original hex value for code to be installed at ' + self.offset )
self._origCode = ''
else:
self._origCode = filteredOriginal
self._origCodePreprocessed = True
return self._origCode
@origCode.setter
def origCode( self, code ):
self._origCode = code
self._origCodePreprocessed = False
@property
def name( self ):
""" This name should be provided from the original file that the code came from.
If that is not available (e.g. for a new change), this will create one based
on the annotation (removing illegal characters), if available. If there is no
annotation, this will create a generic name based on the change type and
address of the code change. The file extension is not included. """
if not self._name:
address = self.offset
anno = self.anno
if anno:
if len( anno ) > 42:
name = anno[:39] + '...'
elif anno:
name = anno
self._name = removeIllegalCharacters( name, '' )
else: # No annotation available
if self.type == 'static':
self._name = 'Static overwrite at {}'.format( address )
elif self.type == 'injection':
self._name = 'Code injection at {}'.format( address )
elif self.type == 'standalone':
self._name = "SA, '{}'".format( address )
else:
self._name = 'Unknown code change at {}'.format( address )
return self._name
def getLength( self ):
if self.length == -1:
self.evaluate()
return self.length
def updateLength( self, newLength ):
self.length = newLength
self.origCode = '' # Will otherwise have the wrong amount of data. Will be recollected when needed
def evaluate( self, reevaluate=False ):
""" Checks for special syntaxes and configurations, ensures configuration options are present and
configured correctly, and assembles source code if it's not already in hex form. Reevaluation
of custom code can be important if the code is changed, or the mod is saved to a new location
(which could potentially change import directories). """
if self.processStatus != -1 and not reevaluate:
return self.processStatus
elif reevaluate:
oldProcessedCode = self.preProcessedCode
else:
oldProcessedCode = ''
#print( 'evaluating {} for {}'.format(self.offset, self.mod.name) )
self.processStatus, codeLength, codeOrErrorNote, self.syntaxInfo, self.isAssembly = globalData.codeProcessor.evaluateCustomCode( self.rawCode, self.mod.includePaths, self.mod.configurations )
self.updateLength( codeLength )
# if self.isAssembly:
# print( self.mod.name + ' includes ASM' )
if self.processStatus == 0:
self.preProcessedCode = codeOrErrorNote
# Check to invalidate the pre-processed code cache
if reevaluate and self.preProcessedCode != oldProcessedCode:
self.isCached = False
# Store a message for the user on the cause
elif self.processStatus == 1:
self.mod.assemblyError = True
if self.type == 'standalone':
self.mod.stateDesc = 'Assembly error with SF "{}"'.format( self.offset )
self.mod.errors.add( 'Assembly error with SF "{}":\n{}'.format(self.offset, codeOrErrorNote) )
elif self.type == 'gecko':
address = self.rawCode.lstrip()[:8]
self.mod.stateDesc = 'Assembly error with gecko code change at {}'.format( address )
self.mod.errors.add( 'Assembly error with gecko code change at {}:\n{}'.format(address, codeOrErrorNote) )
else:
self.mod.stateDesc = 'Assembly error with custom code change at {}'.format( self.offset )
self.mod.errors.add( 'Assembly error with custom code change at {}:\n{}'.format(self.offset, codeOrErrorNote) )
elif self.processStatus == 2:
self.mod.parsingError = True
self.mod.stateDesc = 'Missing include file: {}'.format(codeOrErrorNote)
self.mod.errors.add( 'Missing include file: {}'.format(codeOrErrorNote) )
#self.mod.missingIncludes.append( preProcessedCustomCode ) # todo: implement a way to show these to the user (maybe warning icon & interface)
elif self.processStatus == 3:
self.mod.parsingError = True
if not self.mod.configurations:
self.mod.stateDesc = 'Unable to find configurations'
self.mod.errors.add( 'Unable to find configurations' )
else:
self.mod.stateDesc = 'Configuration option not found: {}'.format(codeOrErrorNote)
self.mod.errors.add( 'Configuration option not found: {}'.format(codeOrErrorNote) )
elif self.processStatus == 4:
self.mod.parsingError = True
self.mod.stateDesc = 'Configuration option "{}" missing type parameter'.format( codeOrErrorNote )
self.mod.errors.add( 'Configuration option "{}" missing type parameter'.format(codeOrErrorNote) )
elif self.processStatus == 5:
self.mod.parsingError = True
self.mod.stateDesc = 'Unrecognized configuration option type: {}'.format(codeOrErrorNote)
self.mod.errors.add( 'Unrecognized configuration option type: {}'.format(codeOrErrorNote) )
if self.processStatus != 0:
self.preProcessedCode = ''
self.isCached = False
print( 'Error parsing code change at', self.offset, ' Error code: {}; {}'.format( self.processStatus, self.mod.stateDesc ) )
return self.processStatus
def loadPreProcCode( self, preProcessedCode, rawBinary ):
""" Loads/imports cached or previously assembled (preProcessed) code and skips some steps
in the .evaluate() method to reduce work; most notably avoids IPC calls to command
line to assemble source code. This is done by loading data from the .bin or .txt files. """
# Use this as the 'custom code' if there isn't any (no .asm file)
if not self.rawCode.strip():
if rawBinary and len( preProcessedCode ) <= 0x100: # This count is in nibbles rather than bytes
self.rawCode = globalData.codeProcessor.beautifyHex( preProcessedCode, 2 )
elif rawBinary:
self.rawCode = globalData.codeProcessor.beautifyHex( preProcessedCode, 4 )
else:
self.rawCode = preProcessedCode
# If loading raw binary from a bin file, there's not much needed to do here!
if rawBinary:
self.length = len( preProcessedCode ) / 2
self.syntaxInfo = []
self.isCached = True
self.processStatus = 0
self.preProcessedCode = preProcessedCode
return 0
# Loading preProcessed code from a txt file; likely has custom syntaxes mixed in that need to be evaluated
codeLines = preProcessedCode.splitlines()
self.isAssembly = globalData.codeProcessor.codeIsAssembly( codeLines )
#assert not self.isAssembly, 'Unexpectedly found ASM when loading preProc code for {}, codeChange 0x{:X}'.format( self.mod.name, self.offset )
# If assembly is found in this file, it's probably meant to be in the .asm file
if self.isAssembly:
print( 'Found assembly in supposedly pre-processed code for "{}"; code change at {}.'.format(self.mod.name, self.offset) )
self.processStatus = -1
self.length = -1
return
self.processStatus, codeLength, codeOrErrorNote, self.syntaxInfo = globalData.codeProcessor._evaluateHexcode( codeLines, self.mod.includePaths, self.mod.configurations )
self.updateLength( codeLength )
if self.processStatus == 0:
self.preProcessedCode = codeOrErrorNote
self.isCached = True
# Store a message for the user on the cause
elif self.processStatus == 3:
self.mod.parsingError = True
if not self.mod.configurations:
self.mod.stateDesc = 'Unable to find configurations'
self.mod.errors.add( 'Unable to find configurations' )
else:
self.mod.stateDesc = 'Configuration option not found: {}'.format(codeOrErrorNote)
self.mod.errors.add( 'Configuration option not found: {}'.format(codeOrErrorNote) )
elif self.processStatus == 4:
self.mod.parsingError = True
self.mod.stateDesc = 'Configuration option "{}" missing type parameter'.format( codeOrErrorNote )
self.mod.errors.add( 'Configuration option "{}" missing type parameter'.format(codeOrErrorNote) )
elif self.processStatus == 5:
self.mod.parsingError = True
self.mod.stateDesc = 'Unrecognized configuration option type: {}'.format(codeOrErrorNote)
self.mod.errors.add( 'Unrecognized configuration option type: {}'.format(codeOrErrorNote) )
if self.processStatus != 0:
self.preProcessedCode = ''
print( 'Error parsing code change at', self.offset, ' Error code: {}; {}'.format( self.processStatus, self.mod.stateDesc ) )
return self.processStatus
def finalizeCode( self, targetAddress, reevaluate=False ):
""" Performs final code processing for custom code, just before saving it to the DOL or codes file.
The save location for the code as well as addresses for any standalone functions it might
require should already be known by this point, so custom syntaxes can now be resolved. User
configuration options are also now saved into the custom code. """
self.evaluate( reevaluate )
if self.mod.errors:
msg( 'Unable to process custom code for {}; {}'.format(self.mod.name, '\n'.join(self.mod.errors)), 'Error During Pre-Processing', warning=True )
return 5, ''
if not self.syntaxInfo:
returnCode = 0
finishedCode = self.preProcessedCode
else:
returnCode, finishedCode = globalData.codeProcessor.resolveCustomSyntaxes( targetAddress, self )
if returnCode != 0 and returnCode != 100: # In cases of an error, 'finishedCode' will include specifics on the problem
if len( self.rawCode ) > 250: # Prevent a very long user message
codeSample = self.rawCode[:250] + '\n...'
else:
codeSample = self.rawCode
errorMsg = 'Unable to process custom code for {}:\n\n{}\n\n{}'.format( self.mod.name, codeSample, finishedCode )
msg( errorMsg, 'Error Resolving Custom Syntaxes' )
elif not finishedCode or not validHex( finishedCode ): # Failsafe; definitely not expected
msg( 'There was an unknown error while processing the following custom code for {}:\n\n{}'.format(self.mod.name, self.rawCode), 'Error During Final Code Processing', warning=True )
returnCode = 6
return returnCode, finishedCode
class CodeMod( object ):
""" Container for all of the information on a code-related game mod. May be sourced from
code stored in the standard MCM format, or the newer ASM Mod Folder Structure (AMFS). """
def __init__( self, name, auth='', desc='', srcPath='', isAmfs=False ):
self.name = name
self.auth = auth # Author(s)
self.desc = desc # Description
self.data = OrderedDict([]) # Keys=revision, values=list of "CodeChange" objects
self.path = os.path.normpath( srcPath ) # Root folder path that contains this mod
self.type = 'static' # An overall type (matches change.types)
self.state = 'disabled'
self.category = ''
self.stateDesc = '' # Describes reason for the state. Shows as a text status on the mod in the GUI
self.configurations = OrderedDict([]) # Will be a dict of option dictionaries. Required keys: type, value, default
# Optional keys: annotation, range, mask, members, hidden
self.isAmfs = isAmfs
self.isMini = False # todo; replace this and above bool with a storeFormat Enum if this format is kept
self.webLinks = [] # A list of tuples, with each of the form ( URL, comment )
self.fileIndex = -1 # Position within a .txt file; used only with MCM formatted mods (non-AMFS)
self.includePaths = []
self.currentRevision = '' # Switch this to set the default revision used to add or get code changes
self.guiModule = None
self.assemblyError = False
self.parsingError = False
#self.missingIncludes = [] # Include filesnames detected to be required by the assembler
self.errors = set()
def setState( self, newState, statusText='', updateControlPanelCounts=True ):
self.state = newState
if self.guiModule:
try:
self.guiModule.setState( newState, statusText, updateControlPanelCounts )
except:
pass # May not be currently displayed in the GUI
def setCurrentRevision( self, revision ):
""" Creates a new code changes list in the data dictionary, and sets
this mod's default revision for getting or adding code changes. """
if revision not in self.data:
self.data[revision] = []
self.currentRevision = revision
def getCodeChanges( self, forAllRevisions=False, revision='' ):
""" Gets all code changes required for a mod to be installed. """
codeChanges = []
if forAllRevisions:
for changes in self.data.values():
codeChanges.extend( changes )
elif revision:
# Get code changes that are applicable to all revisions, as well as those applicable to just the requested revision
codeChanges.extend( self.data.get('ALL', []) )
codeChanges.extend( self.data.get(revision, []) )
else:
# Get code changes that are applicable to all revisions, as well as those applicable to just the currently set revision
codeChanges.extend( self.data.get('ALL', []) )
codeChanges.extend( self.data.get(self.currentRevision, []) )
return codeChanges
def _normalizeCodeImport( self, customCode, annotation='' ):
""" Normalize custom code import; ensure it's a string, removing
leading and trailing whitespace, and create an annotation
from the code if one isn't provided. """
if customCode:
# Collapse the list of collected code lines into one string, removing leading & trailing whitespace
if isinstance( customCode, list ):
customCode = '\n'.join( customCode )
customCode = customCode.strip()
# See if we can get an annotation
if not annotation:
firstLine = customCode.splitlines()[0].rstrip()
if firstLine.startswith( '#' ):
annotation = firstLine.strip( '#' ).lstrip()
else: # Could still be an empty list. Make sure it's a string
customCode = ''
return customCode, annotation
def addStaticOverwrite( self, offsetString, customCode, origCode='', annotation='', name='' ):
# Collapse the list of collected code lines into one string, removing leading & trailing whitespace
customCode, annotation = self._normalizeCodeImport( customCode, annotation )
# Add the code change
codeChange = CodeChange( self, 'static', offsetString, origCode, customCode, annotation, name )
self.data[self.currentRevision].append( codeChange )
return codeChange
def addInjection( self, offsetString, customCode, origCode='', annotation='', name='' ):
# Collapse the list of collected code lines into one string, removing leading & trailing whitespace
customCode, annotation = self._normalizeCodeImport( customCode, annotation )
# Add the code change
codeChange = CodeChange( self, 'injection', offsetString, origCode, customCode, annotation, name )
self.data[self.currentRevision].append( codeChange )
if self.type == 'static': # 'static' is the only type that 'injection' can override.
self.type = 'injection'
return codeChange
def addGecko( self, customCode, annotation='', name='' ):
""" This is for Gecko codes that could not be converted into strictly static
overwrites and/or injection mods. These will require the Gecko codehandler. """
# Collapse the list of collected code lines into one string, removing leading & trailing whitespace
customCode, annotation = self._normalizeCodeImport( customCode, annotation )
# Add the code change
codeChange = CodeChange( self, 'gecko', '', '', customCode, annotation, name )
self.data[self.currentRevision].append( codeChange )
self.type = 'gecko'
return codeChange
def addStandalone( self, standaloneName, standaloneRevisions, customCode, annotation='', name='' ):
# Collapse the list of collected code lines into one string, removing leading & trailing whitespace
customCode, annotation = self._normalizeCodeImport( customCode, annotation )
# Add the code change for each revision that it was defined for
codeChange = CodeChange( self, 'standalone', standaloneName, '', customCode, annotation, name )
for revision in standaloneRevisions:
if revision not in self.data:
self.data[revision] = []
self.data[revision].append( codeChange )
codeChange.evaluate()
# Save this SF in the global dictionary
if codeChange.processStatus == 0 and standaloneName not in globalData.standaloneFunctions:
globalData.standaloneFunctions[standaloneName] = ( -1, codeChange )
self.type = 'standalone'
return codeChange
def _parseCodeForStandalones( self, codeChange, requiredFunctions, missingFunctions ):
""" Recursive helper function for getRequiredStandaloneFunctionNames(). Checks
one particular code change (injection/overwrite) for standalone functions. """
for syntaxOffset, length, syntaxType, codeLine, names in codeChange.syntaxInfo:
if syntaxType == 'sbs' and '<' in codeLine and '>' in codeLine: # Special Branch Syntax; one name expected
newFunctionNames = ( codeLine.split( '<' )[1].split( '>' )[0], ) # Second split prevents capturing comments following on the same line.
elif syntaxType == 'sym': # These lines could have multiple names
newFunctionNames = []
for fragment in codeLine.split( '<<' ):
if '>>' in fragment: # A symbol (function name) is in this string segment.
newFunctionNames.append( fragment.split( '>>' )[0] )
else: continue
for functionName in newFunctionNames:
# Skip this function if it has already been analyzed
if functionName in requiredFunctions:
continue
requiredFunctions.append( functionName )
# Recursively check for more functions that this function may reference
functionMapping = globalData.standaloneFunctions.get( functionName )
if functionMapping:
codeChange = functionMapping[1] # First item is function address (if allocated; -1 if not)
self._parseCodeForStandalones( codeChange, requiredFunctions, missingFunctions )
elif functionName not in missingFunctions:
missingFunctions.append( functionName )
return requiredFunctions, missingFunctions
def getRequiredStandaloneFunctionNames( self ):
""" Gets the names of all standalone functions a particular mod requires.
Returns a list of these function names, as well as a list of any missing functions. """
functionNames = []
missingFunctions = []
# This loop will be over a list of tuples (code changes) for a specific game version.
for codeChange in self.getCodeChanges():
if codeChange.type != 'gecko': #todo allow gecko codes to have SFs
codeChange.evaluate()
if codeChange.syntaxInfo:
self._parseCodeForStandalones( codeChange, functionNames, missingFunctions )
return functionNames, missingFunctions # functionNames will also include those that are missing
def clearErrors( self ):
self.assemblyError = False
self.parsingError = False
self.stateDesc = ''
self.errors.clear()
def validateConfigurations( self ):
""" Ensures all configurations options include at least 'type', 'value', and 'default' parameters.
Removes the configuration option from the dictionary if any of these are missing. """
assert isinstance( self.configurations, dict ), 'Invalid mod configuration! The configurations property should be a dictionary.'
badConfigs = []
for configName, dictionary in self.configurations.items():
if 'type' not in dictionary:
self.parsingError = True
self.stateDesc = 'Configuration option "{}" missing type parameter'.format( configName )
self.errors.add( 'Configuration option "{}" missing type parameter'.format(configName) )
badConfigs.append( configName )
if 'value' not in dictionary:
self.parsingError = True
self.stateDesc = 'Configuration option "{}" missing value parameter'.format( configName )
self.errors.add( 'Configuration option "{}" missing value parameter'.format(configName) )
badConfigs.append( configName )
if 'default' not in dictionary:
self.parsingError = True
self.stateDesc = 'Configuration option "{}" missing default parameter'.format( configName )
self.errors.add( 'Configuration option "{}" missing default parameter'.format(configName) )
badConfigs.append( configName )
# Remove bad configurations to prevent creating other bugs in the program
if badConfigs:
for config in badConfigs:
del self.configurations[config]
def configure( self, name, value ):
""" Changes a given configuration option to the given value. """
# for option in self.configurations:
# if option['name'] == name:
# option['value'] = value
# break
# else:
# raise Exception( '{} not found in configuration options.'.format(name) )
self.configurations[name]['value'] = value
def getConfiguration( self, name ):
""" Gets the currently-set configuration option for a given option name. """
# for option in self.configurations:
# if option['name'] == name:
# return option
# else:
# raise Exception( '{} not found in configuration options.'.format(name) )
return self.configurations.get( name )
def getConfigValue( self, name ):
return self.configurations[name]['value']
@staticmethod
def parseConfigValue( optionType, value ):
""" Normalizes value input that may be a hex/decimal string or an int/float literal
to an int or float. The source value type may not be consistent due to
varying sources (i.e. from an MCM format file or AMFS config/json file). """
if not isinstance( value, (int, float, long) ): # Need to typecast to int or float
if '0x' in value: # Convert from hex using base 16
value = int( value, 16 )
elif optionType == 'float':
value = float( value )
else: # Assume decimal value
value = int( value )
return value
def restoreConfigDefaults( self ):
""" Restores all configuration values to the mod's default values. """
for dictionary in self.configurations.values():
dictionary['value'] = dictionary['default']
def assembleErrorMessage( self, includeStateDesc=False ):
errorMsg = []
if includeStateDesc:
errorMsg.append( self.stateDesc + '\n' )
if self.parsingError:
errorMsg.append( 'Parsing Errors Detected: Yes' )
else:
errorMsg.append( 'Parsing Errors Detected: No' )
if self.assemblyError:
errorMsg.append( 'Assembly Errors Detected: Yes\n' )
else:
errorMsg.append( 'Assembly Errors Detected: No\n' )
errorMsg.extend( self.errors )
return '\n'.join( errorMsg )
def assessForErrors( self ):
""" Evaluates this mod's custom code for assembly errors and checks for valid DOL offsets. """
# Get the DOL file
try:
dol = globalData.getVanillaDol()
except Exception as err:
printStatus( 'Unable to get DOL to assess code offsets/addresses; {}'.format(err.message), warning=True )
dol = None
for codeChanges in self.data.values():
for change in codeChanges:
# Filter out SAs and Gecko codes
if change.type == 'standalone' or change.type == 'gecko':
continue
# Check if the RAM Address or DOL Offset is valid
if dol:
error = dol.normalizeDolOffset( change.offset, 'string' )[1]
if error:
self.parsingError = True
self.errors.add( error )
# Check for assembly errors
change.evaluate( True )
def assessForConflicts( self, silent=False, revision='' ):
""" Evaluates this mod's changes to look for internal overwrite conflicts
(i.e. more than one change that affects the same code space).
Returns True or False on whether a conflict was detected. """
dol = globalData.getVanillaDol()
conflictDetected = False
modifiedRegions = []
for change in self.getCodeChanges( revision=revision ):
# Filter out SAs and Gecko codes
if change.type == 'standalone' or change.type == 'gecko':
continue
# Ensure a RAM address is available or can be determined
ramAddress, errorMsg = dol.normalizeRamAddress( change.offset )
if ramAddress == -1:
if not revision:
revision = self.currentRevision
warningMsg = 'Unable to get a RAM Address for the code change at {} ({});{}.'.format( change.offset, revision, errorMsg.split(';')[1] )
if not silent:
msg( warningMsg, 'Invalid DOL Offset or RAM Address', warning=True )
self.stateDesc = 'Invalid Offset or Address'
self.errors.add( warningMsg )
break
if change.type == 'injection':
addressEnd = ramAddress + 4
else:
addressEnd = ramAddress + change.getLength()
# Check if this change overlaps other regions collected so far
for regionStart, codeLength in modifiedRegions:
regionEnd = regionStart + codeLength
if ramAddress < regionEnd and regionStart < addressEnd: # The regions overlap by some amount.
conflictDetected = True
break
# No overlap, store this region this change affects for the next iterations
if change.type == 'injection':
modifiedRegions.append( (ramAddress, 4) )
else: # Static overwrite
modifiedRegions.append( (ramAddress, change.length) )
if conflictDetected:
break
if conflictDetected:
# Construct a warning message to the user
if regionStart == ramAddress:
dolOffset = dol.normalizeDolOffset( change.offset, 'string' )[0]
warningMsg = '{} has code that conflicts with itself. More than one code change modifies code at 0x{:X} ({}).'.format( self.name, ramAddress, dolOffset )
else:
oldChangeRegion = 'Code Start: 0x{:X}, Code End: 0x{:X}'.format( regionStart, regionEnd )
newChangeRegion = 'Code Start: 0x{:X}, Code End: 0x{:X}'.format( ramAddress, addressEnd )
warningMsg = ('{} has code that conflicts with itself. These two code changes '
'overlap with each other:\n\n{}\n{}').format( self.name, oldChangeRegion, newChangeRegion )
if not silent:
msg( warningMsg, 'Code Conflicts Detected', warning=True )
self.stateDesc = 'Code Conflicts Detected'
self.errors.add( warningMsg )
return conflictDetected
def diagnostics( self, level=1, silent=False ):
self.assessForErrors()
for revision in self.data.keys():
self.assessForConflicts( silent, revision )
#if level >= 2:
def validateWebLink( self, origUrlString ):
""" Validates a given URL (string), partly based on a whitelist of allowed domains.
Returns a urlparse object if the url is valid, or None (Python default) if it isn't. """
try:
potentialLink = urlparse.urlparse( origUrlString )
except Exception as err:
print( 'Invalid link detected for "{}": {}'.format(self.name, err) )
return
# Check the domain against the whitelist. netloc will be something like "youtube.com" or "www.youtube.com"
if potentialLink.scheme and potentialLink.netloc.split('.')[-2] in ( 'smashboards', 'github', 'youtube' ):
return potentialLink
elif not potentialLink.scheme:
print( 'Invalid link detected for "{}" (no scheme): {}'.format(self.name, origUrlString) )
else:
print( 'Invalid link detected for "{}" (domain not allowed): {}'.format(self.name, origUrlString) )
def buildModString( self ):
""" Builds a string to store/share this mod in MCM's original, text-file code format.
If this mod is a Gecko code, this method will create a MCM-Gecko format string that is
a slight variant of a normal Gecko code (as one would see in a Dolphin INI file). This
variant exists so that a Gecko code may have several variants for different revisions. """
# Collect lines for title, description, and web links
if self.name:
headerLines = [ self.name ]
else:
headerLines = [ 'This Mod Needs a Title!' ]
if self.desc:
headerLines.append( self.desc )
if self.webLinks:
for urlString, comments in self.webLinks: # Comments should still have the '#' character prepended
if not comments:
headerLines.append( '<{}>'.format(urlString) )
elif comments.lstrip()[0] == '#':
headerLines.append( '<{}>{}'.format(urlString, comments) )
else:
headerLines.append( '<{}> # {}'.format(urlString, comments) )
# Add configuration definitions
if self.configurations:
headerLines.append( 'Configurations:' )
for name, definition in self.configurations.items(): # ConfigurationTypes
# Collect optional keys
comment = definition.get( 'annotation', '' )
valueRange = definition.get( 'range', '' )
mask = definition.get( 'mask', '' )
members = definition.get( 'members', '' )
titleLine = ' {} {} = {}'.format( definition['type'], name, definition['value'] ) # Current value will be set as default!
if valueRange:
titleLine += '; {}-{}'.format( valueRange[0], valueRange[1] )
elif mask:
titleLine += ' (0x{:X})'.format( mask )
if comment:
titleLine += ' ' + comment.lstrip()
headerLines.append( titleLine )
for components in members:
if len( components ) == 2:
name, value = components
headerLines.append( ' {}: {}'.format(value, name) )
else:
name, value, comment = components
line = ' {}: {}'.format( value, name )
if comment: # Expected to still have "#" prepended
line += ' ' + comment.lstrip()
headerLines.append( line )
if self.auth:
headerLines.append( '[' + self.auth + ']' )
else:
headerLines.append( '[??]' )
codeChangesHeader = 'Revision ---- DOL Offset ---- Hex to Replace ---------- ASM Code -'
addChangesHeader = False
codeChangeLines = []
for revision, codeChanges in self.data.items():
addVersionHeader = True
for change in codeChanges:
newHex = change.rawCode.strip()
if not newHex:
continue
if change.type in ( 'static', 'injection' ):
change.evaluate( True )
addChangesHeader = True
# Get the offset
if change.offset:
offset = change.offset.replace( '0x', '' )
else:
offset = '1234' # Placeholder; needed so the mod can still be parsed/re-loaded
# Get the original hex (vanilla game code)
if change.origCode:
originalHex = ''.join( change.origCode.split() ).replace( '0x', '' )
if not validHex( originalHex ):
msg( 'There is missing or invalid Original Hex code\nfor some ' + revision + ' changes.' )
return ''
else:
codeLength = change.getLength()
if codeLength == -1:
originalHex = '00000000' # Placeholder
else:
originalHex = '00' * codeLength
# Create the beginning of the line (revision header, if needed, with dashes).
headerLength = 13
if addVersionHeader:
lineHeader = revision + ' ' + ('-' * ( headerLength - 1 - len(revision) )) # extra '- 1' for the space after revision
addVersionHeader = False
else: lineHeader = '-' * headerLength
# Build a string for the offset portion
numOfDashes = 8 - len( offset )
dashes = '-' * ( numOfDashes / 2 ) # if numOfDashes is 1 or less (including negatives), this will be an empty string
if numOfDashes % 2 == 0: # If the number of dashes left over is even (0 is even)
offsetString = dashes + ' ' + '0x' + offset + ' ' + dashes
else: # Add an extra dash at the end (the int division above rounds down)
offsetString = dashes + ' ' + '0x' + offset + ' ' + dashes + '-'
# Build a string for a standard (short) static overwrite
if change.type == 'static' and len( originalHex ) <= 16 and newHex.splitlines()[0].split('#')[0] != '': # Last check ensures there's actually code, and not just comments/whitespace
codeChangeLines.append( lineHeader + offsetString + '---- ' + originalHex + ' -> ' + newHex )
# Long static overwrite
elif change.type == 'static':
prettyHex = globalData.codeProcessor.beautifyHex( originalHex )
codeChangeLines.append( lineHeader + offsetString + '----\n\n' + prettyHex + '\n\n -> \n\n' + newHex + '\n' )
# Injection mod
else:
codeChangeLines.append( lineHeader + offsetString + '---- ' + originalHex + ' -> Branch\n\n' + newHex + '\n' )
elif change.type == 'gecko':
if addVersionHeader: codeChangeLines.append( revision + '\n' + newHex + '\n' )
else: codeChangeLines.append( newHex + '\n' )
elif change.type == 'standalone':
functionName = change.offset
# Check that a function name was given, and then convert the ASM to hex if necessary.
if functionName == '':
msg( 'A standalone function among the ' + revision + ' changes is missing a name.' )
return ''
elif ' ' in functionName:
msg( 'Function names may not contain spaces. Please rename those for ' + revision + ' and try again.' )
return ''
# Add the name wrapper and version identifier
functionName = '<' + functionName + '> ' + revision
# Assemble the line string with the original and new hex codes.
codeChangeLines.append( functionName + '\n' + newHex + '\n' )
if addChangesHeader:
headerLines.append( codeChangesHeader )
return '\n'.join( headerLines + codeChangeLines )
def buildGeckoString( self, vanillaDol, createForGCT ):
""" Formats a mod's code into Gecko code form. Note that this is the 'true' or original Gecko code format
(as Dolphin would use), not the MCM-Gecko variant. If this is for an INI file, human-readable mod-name/author headers and
whitespace are included. If this is for a GCT file, it'll just be pure hex data (though returned as a string). """
containsSpecialSyntax = False
codeChanges = []
for codeChange in self.data[vanillaDol.revision]:
codeChange.evaluate() # Ensure code length has been determined
# Check for special syntaxes; only one kind can be adapted for use by Gecko codes (references to SFs)
if codeChange.syntaxInfo or codeChange.type == 'standalone': # Not supported for Gecko codes
containsSpecialSyntax = True
break
elif codeChange.type == 'static':
# Determine the Gecko operation code
if codeChange.length == 1:
opCode = 0
filling = '000000'
elif codeChange.length == 2:
opCode = 2
filling = '0000'
elif codeChange.length == 4:
opCode = 4
filling = ''
else:
opCode = 6
filling = toHex( codeChange.length, 8 ) # Pads a hex string to 8 characters long (extra characters added to left side)
ramAddress = vanillaDol.normalizeRamAddress( codeChange.offset )[0]
if ramAddress > 0x81000000:
opCode += 1
ramAddress = ramAddress & 0x1FFFFFF # Mask out base address
firstWord = '{:02X}{:06X}'.format( opCode, ramAddress )
if createForGCT:
codeChanges.append( firstWord + filling + codeChange.preProcessedCode )
elif codeChange.length > 4:
sByteCount = toHex( codeChange.length, 8 ) # Pads a hex string to 8 characters long (extra characters added to left side)
beautifiedHex = globalData.codeProcessor.beautifyHex( codeChange.preProcessedCode )
codeChanges.append( firstWord + ' ' + sByteCount + '\n' + beautifiedHex )
else:
codeChanges.append( firstWord + ' ' + filling + codeChange.preProcessedCode )
elif codeChange.type == 'injection':
opCode = codeChange.preProcessedCode[-8:][:-6].lower() # Of the last instruction
ramAddress = vanillaDol.normalizeRamAddress( codeChange.offset )[0]
sRamAddress = toHex( ramAddress, 6 ) # Pads a hex string to 6 characters long (extra characters added to left side)
# todo: +1 to opcode if address > 0x81000000
# if ramAddress > 0x81000000:
# opCode += 1
if createForGCT:
# Check the last instruction; it may be a branch placeholder, which may be removed
if opCode in ( '48', '49', '4a', '4b', '00' ):
codeChange.preProcessedCode = codeChange.preProcessedCode[:-8]