forked from tzlion/frogtool
-
Notifications
You must be signed in to change notification settings - Fork 11
/
tadpole_functions.py
1492 lines (1347 loc) · 61.2 KB
/
tadpole_functions.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
# GUI imports
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
# OS imports
import os
import shutil
import hashlib
import zipfile
from io import BytesIO
#feature imports
import struct
import frogtool
import requests
import json
import logging
import time
try:
from PIL import Image, ImageDraw, ImageFont
image_lib_avail = True
except ImportError:
Image = None
ImageDraw = None
image_lib_avail = False
# This dictionary is in the following format:
# "System": []
systems = {
"FC": ["rdbui.tax", "fhcfg.nec", "nethn.bvs",1],
"SFC": ["urefs.tax", "adsnt.nec", "xvb6c.bvs",2],
"MD": ["scksp.tax", "setxa.nec", "wmiui.bvs",3],
"GB": ["vdsdc.tax", "umboa.nec", "qdvd6.bvs",4],
"GBC": ["pnpui.tax", "wjere.nec", "mgdel.bvs",5],
"GBA": ["vfnet.tax", "htuiw.nec", "sppnp.bvs",6],
"ARCADE": ["mswb7.tax", "msdtc.nec", "mfpmp.bvs",7]
}
supported_save_ext = [
"sav", "sa0", "sa1", "sa2", "sa3"
]
version_displayString_1_5 = "2023.04.20 (V1.5)"
version_displayString_1_6 = "2023.08.03 (V1.6)"
version_displayString_1_7 = "2023.10.07 (V1.7)"
version_displayString_1_71 ="2023.10.13 (V1.71)"
# hash, versionName
versionDictionary = {
"151d5eeac148cbede3acba28823c65a34369d31b61c54bdd8ad049767d1c3697": version_displayString_1_5,
"5335860d13214484eeb1260db8fe322efc87983b425ac5a5f8b0fcdf9588f40a": version_displayString_1_6,
"b88458bf2c25d3a34ab57ee149f36cfdc6b8a5138d5c6ed147fbea008b4659db": version_displayString_1_7,
"08bd07ab3313e3f00b922538516a61b5846cde34c74ebc0020cd1a0b557dd54b": version_displayString_1_71
}
ROMART_baseURL = "https://raw.githubusercontent.com/EricGoldsteinNz/libretro-thumbnails/master/"
ROMArt_console = {
"FC": "Nintendo - Nintendo Entertainment System",
"SFC": "Nintendo - Super Nintendo Entertainment System",
"MD": "Sega - Mega Drive - Genesis",
"GB": "Nintendo - Game Boy",
"GBC": "Nintendo - Game Boy Color",
"GBA": "Nintendo - Game Boy Advance",
"ARCADE": ""
}
offset_logo_presequence = [0x62, 0x61, 0x64, 0x5F, 0x65, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x00, 0x00]
offset_buttonMap_presequence = [0x00, 0x00, 0x00, 0x71, 0xDB, 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
offset_buttonMap_postsequence = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]
class Exception_InvalidPath(Exception):
pass
class Exception_StopExecution(Exception):
pass
class InvalidURLError(Exception):
pass
def changeBootLogo(index_path, newLogoFileName, msgBox):
# Confirm we arent going to brick the firmware by finding a known version
sfVersion = bisrv_getFirmwareVersion(index_path)
print(f"Found Version: {sfVersion}")
if sfVersion == None:
return False
# Load the new Logo
msgBox.setText("Uploading new boot logo...")
msgBox.showProgress(25, True)
newLogo = QImage(newLogoFileName)
# Convert to RGB565
msgBox.setText("Converting boot logo...")
msgBox.showProgress(40, True)
rgb565Data = QImageToRGB565Logo(newLogo)
# Change the boot logo
msgBox.setText("Uploading boot logo...")
msgBox.showProgress(60, True)
file_handle = open(index_path, 'rb') # rb for read, wb for write
bisrv_content = bytearray(file_handle.read(os.path.getsize(index_path)))
file_handle.close()
logoOffset = findSequence(offset_logo_presequence, bisrv_content,10000000)
bootLogoStart = logoOffset + 16
for i in range(0, 512*200):
data = rgb565Data[i].to_bytes(2, 'little')
bisrv_content[bootLogoStart+i*2] = data[0]
bisrv_content[bootLogoStart+i*2+1] = data[1]
msgBox.setText("Updating BIOS file...")
msgBox.showProgress(80, True)
print("Patching CRC")
bisrv_content = patchCRC32(bisrv_content)
msgBox.setText("Uploading BIOS file...")
msgBox.showProgress(90, True)
print("Writing bisrv to file")
file_handle = open(index_path, 'wb') # rb for read, wb for write
file_handle.write(bisrv_content)
file_handle.close()
msgBox.showProgress(99, True)
return True
def patchCRC32(bisrv_content):
x = crc32mpeg2(bisrv_content[512:len(bisrv_content):1])
bisrv_content[0x18c] = x & 255
bisrv_content[0x18d] = x >> 8 & 255
bisrv_content[0x18e] = x >> 16 & 255
bisrv_content[0x18f] = x >> 24
return bisrv_content
def crc32mpeg2(buf, crc=0xffffffff):
for val in buf:
crc ^= val << 24
for _ in range(8):
crc = crc << 1 if (crc & 0x80000000) == 0 else (crc << 1) ^ 0x104c11db7
return crc
def QImageToRGB565Logo(inputQImage):
print("Converting supplied file to boot logo format")
# Need to increase the size to 512x200
inputQImage = inputQImage.scaled(512, 200, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
inputQImage = inputQImage.convertToFormat(QImage.Format_RGB16)
rgb565Data = []
for y in range(0, 200):
for x in range(0, 512):
pixel = inputQImage.pixelColor(x,y)
pxValue = ((pixel.red() & 248) << 8) + ((pixel.green() & 252) << 3) + (pixel.blue() >> 3)
rgb565Data.append(pxValue)
print("Finished converting image to boot logo format")
return rgb565Data
def changeZIPThumbnail(romPath, newImpagePath, system):
try:
newLogoPath = os.path.dirname(romPath)
newLogoName = os.path.basename(newImpagePath)
romFile = os.path.basename(romPath)
new_romPath = os.path.dirname(romPath)
newLogoFile = os.path.join(newLogoPath,newLogoName)
shutil.copyfile(newImpagePath, newLogoFile)
sys_zxx_ext = frogtool.zxx_ext[system]
zxx_file_name = f"{frogtool.strip_file_extension(romFile)}.{sys_zxx_ext}"
zxx_file_path = os.path.join(new_romPath,zxx_file_name)
converted = frogtool.rgb565_convert(newLogoFile, zxx_file_path, (144, 208))
if not converted:
return False
zxx_file_handle = open(zxx_file_path, "ab")
zip_file_handle = open(romPath, "rb")
zxx_file_handle.write(zip_file_handle.read())
zxx_file_handle.close()
zip_file_handle.close()
except Exception as e:
print(f"! Failed changing zip file")
logging.error("Could not change thumbnail" + str(e))
return False
try:
os.remove(newLogoFile)
os.remove(romPath)
except (OSError, IOError):
print(f"! Failed deleting source file(s) after creating {zxx_file_name}")
return False
return True
def changeZXXThumbnail(romPath, imagePath):
tempPath = f"{romPath}.tmp"
converted = frogtool.rgb565_convert(imagePath, tempPath, (144, 208))
if not converted:
return False
# copy the rom data to the temp
try:
temp_file_handle = open(tempPath, "ab")
zxx_file_handle = open(romPath, "rb")
romData = bytearray(zxx_file_handle.read())
temp_file_handle.write(romData[59904:])
temp_file_handle.close()
zxx_file_handle.close()
except (OSError, IOError):
print(f"! Failed appending zip file to ")
return False
try:
shutil.move(tempPath,romPath)
except (OSError, IOError) as error:
print(f"! Failed moving temp files. {error}")
return False
return True
def overwriteZXXThumbnail(roms_path, system, progress):
#First we need to get lists of all the images and ROMS
img_files = os.scandir(roms_path)
img_files = list(filter(frogtool.check_img, img_files))
rom_files = os.scandir(roms_path)
rom_files = list(filter(frogtool.check_rom, rom_files))
sys_zxx_ext = frogtool.zxx_ext[system]
if not img_files or not rom_files:
return
print(f"Found image and .z** files, looking for matches to combine to {sys_zxx_ext}")
#SECOND we need to get the RAW copies of each image...if there is a matching Z**
imgs_processed = 0
progress.setMaximum(len(img_files))
progress.setValue(imgs_processed)
for img_file in img_files:
zxx_rom_file = frogtool.find_matching_file_diff_ext(img_file, rom_files)
if not zxx_rom_file:
continue
tempPath = f"{zxx_rom_file.path}.tmp"
converted = frogtool.rgb565_convert(img_file.path, tempPath, (144, 208))
if not converted:
print("! Aborting image processing due to errors")
break
try:
temp_file_handle = open(tempPath, "ab")
zxx_file_handle = open(zxx_rom_file.path, "rb")
romData = bytearray(zxx_file_handle.read())
temp_file_handle.write(romData[59904:])
temp_file_handle.close()
zxx_file_handle.close()
except (OSError, IOError):
print(f"! Failed appending zip file to ")
break
try:
shutil.move(tempPath,zxx_rom_file.path)
except (OSError, IOError) as error:
print(f"! Failed moving temp files. {error}")
break
imgs_processed += 1
progress.setValue(imgs_processed)
QApplication.processEvents()
#Third we need to copy the data of the new thumbnail over to the rom file
"""
This is a rewrtite attempt at changing the cover art inplace rather thancopy and replace
"""
# def changeZXXThumbnail2(romPath, imagePath):
# coverData = getImageData565(imagePath, (144, 208))
# if not coverData:
# return False
# # copy the rom data to the temp
# try:
# zxx_file_handle = open(romPath, "r+b")
# zxx_file_handle.seek(0)
# zxx_file_handle.write(coverData)
# zxx_file_handle.close()
# except (OSError, IOError):
# print(f"! Failed appending zip file to ")
# return False
# return True
def getImageData565(src_filename, dest_size=None):
if not image_lib_avail:
print("! Pillow module not found, can't do image conversion")
return False
try:
srcimage = Image.open(src_filename)
except (OSError, IOError):
print(f"! Failed opening image file {src_filename} for conversion")
return False
# convert the image to RGB if it was not already
image = Image.new('RGB', srcimage.size, (0, 0, 0))
image.paste(srcimage, None)
if dest_size and image.size != dest_size:
#TODO: let user decide to stretch or not
maxsize = (144, 208)
image = image.thumbnail(maxsize, Image.ANTIALIAS)
image_height = image.size[1]
image_width = image.size[0]
pixels = image.load()
if not pixels:
print(f"! Failed to load image from {src_filename}")
return False
rgb565Data = []
for h in range(image_height):
for w in range(image_width):
pixel = pixels[w, h]
if not type(pixel) is tuple:
print(f"! Unexpected pixel type at {w}x{h} from {src_filename}")
return False
r = pixel[0] >> 3
g = pixel[1] >> 2
b = pixel[2] >> 3
rgb = (r << 11) | (g << 5) | b
rgb565Data.append(struct.pack('H', rgb))
return rgb565Data
def bisrv_getFirmwareVersion(index_path):
print(f"trying to read {index_path}")
try:
file_handle = open(index_path, 'rb') # rb for read, wb for write
bisrv_content = bytearray(file_handle.read(os.path.getsize(index_path)))
file_handle.close()
print("Finished reading file")
# First, replace CRC32 bits with 00...
bisrv_content[396] = 0x00
bisrv_content[397] = 0x00
bisrv_content[398] = 0x00
bisrv_content[399] = 0x00
print("Blanked CRC32")
# Next identify the boot logo position, and blank it out too...
print("start finding logo")
badExceptionOffset = findSequence(offset_logo_presequence, bisrv_content, 10000000) #Set the offset to 10000000 as we know it doesnt occur earlier than that
print(f"finished finding logo - ({badExceptionOffset})")
if (badExceptionOffset > -1): # Check we found the boot logo position
bootLogoStart = badExceptionOffset + 16
for i in range(bootLogoStart, bootLogoStart + 204800):
bisrv_content[i] = 0x00
else: # If no boot logo found exit
return False
print("Blanked Bootlogo")
print("start finding button mapping")
# Next identify the emulator button mappings (if they exist), and blank them out too...
preButtonMapOffset = findSequence(offset_buttonMap_presequence, bisrv_content, 9200000)
print(f"found button mapping - ({preButtonMapOffset})")
if preButtonMapOffset > -1:
postButtonMapOffset = findSequence(offset_buttonMap_postsequence, bisrv_content, preButtonMapOffset)
if postButtonMapOffset > -1:
for i in range(preButtonMapOffset + 16, i < postButtonMapOffset):
bisrv_content[i] = 0x00
else:
return False
else:
return False
print("finished finding button mapping")
# Next we'll look for (and zero out) the five bytes that the power
# monitoring functions of the SF2000 use for switching the UI's battery
# level indicator. These unfortunately can't be searched for - they're just
# in specific known locations for specific firmware versions...
print("start finding powercurve")
prePowerCurve = findSequence([0x11, 0x05, 0x00, 0x02, 0x24], bisrv_content,3000000)
print(f"found pre-powercurve - ({prePowerCurve})")
if prePowerCurve > -1:
powerCurveFirstByteLocation = prePowerCurve + 5
if powerCurveFirstByteLocation == 0x35A8F8:
# Seems to match mid-March layout...
bisrv_content[0x35A8F8] = 0x00
bisrv_content[0x35A900] = 0x00
bisrv_content[0x35A9B0] = 0x00
bisrv_content[0x35A9B8] = 0x00
bisrv_content[0x35A9D4] = 0x00
elif powerCurveFirstByteLocation == 0x35A954:
# Seems to match April 20th layout...
bisrv_content[0x35A954] = 0x00
bisrv_content[0x35A95C] = 0x00
bisrv_content[0x35AA0C] = 0x00
bisrv_content[0x35AA14] = 0x00
bisrv_content[0x35AA30] = 0x00
elif powerCurveFirstByteLocation == 0x35C78C:
# Seems to match May 15th layout...
bisrv_content[0x35C78C] = 0x00
bisrv_content[0x35C794] = 0x00
bisrv_content[0x35C844] = 0x00
bisrv_content[0x35C84C] = 0x00
bisrv_content[0x35C868] = 0x00
elif powerCurveFirstByteLocation == 0x35C790:
# Seems to match May 22nd layout...
bisrv_content[0x35C790] = 0x00
bisrv_content[0x35C798] = 0x00
bisrv_content[0x35C848] = 0x00
bisrv_content[0x35C850] = 0x00
bisrv_content[0x35C86C] = 0x00
elif powerCurveFirstByteLocation == 0x3564EC:
# Seems to match August 3rd layout...
bisrv_content[0x3564EC] = 0x00
bisrv_content[0x3564F4] = 0x00
bisrv_content[0x35658C] = 0x00
bisrv_content[0x356594] = 0x00
bisrv_content[0x3565B0] = 0x00
elif powerCurveFirstByteLocation == 0x356638:
#Seems to match October 7th/13th layout...
bisrv_content[0x356638] = 0x00
bisrv_content[0x356640] = 0x00
bisrv_content[0x3566D8] = 0x00
bisrv_content[0x3566E0] = 0x00
bisrv_content[0x3566FC] = 0x00
else:
return False
else:
return False
# Next we'll look for and zero out the bytes used for SNES audio rate and
# CPU cycles, in case folks want to patch those bytes to correct SNES
# first-launch issues on newer firmwares...
# Location: Approximately 0xC0A170 (about 99% of the way through the file)
preSNESBytes = findSequence([0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80], bisrv_content,12500000)
print(f"found pre SNES fix bytes - ({preSNESBytes})")
if preSNESBytes > -1:
snesAudioBitrateBytes = preSNESBytes + 8
snesCPUCyclesBytes = snesAudioBitrateBytes + 8
bisrv_content[snesAudioBitrateBytes] = 0x00
bisrv_content[snesAudioBitrateBytes + 1] = 0x00
bisrv_content[snesCPUCyclesBytes] = 0x00
bisrv_content[snesCPUCyclesBytes + 1] = 0x00
else:
return False
# If we're here, we've zeroed-out all of the bits of the firmware that are
# semi-user modifiable (boot logo, button mappings and the CRC32 bits); now
# we can generate a hash of what's left and compare it against some known
# values...
print("starting to compute hash")
sha256hasher = hashlib.new('sha256')
sha256hasher.update(bisrv_content)
bisrvHash = sha256hasher.hexdigest()
print(f"Hash: {bisrvHash}")
version = versionDictionary.get(bisrvHash)
return version
except (IOError, OSError):
print("! Failed reading bisrv.")
print(" Check the SD card and file are readable, and the file is not open in another program.")
raise Exception_InvalidPath
class Exception_InvalidConsole(Exception):
pass
class Exception_InvalidGamePosition(Exception):
pass
"""
index_path should be the Drive of the Frog card only. It must inlude the semicolon if relevant. ie "E:"
console must be a supported console from the tadpole_functions systems array.
position is a 0-based index of the short. values 0 to 3 are considered valid.
game should be the file name including extension. ie Final Fantasy Tactics Advance (USA).zgb
"""
def changeGameShortcut(drive, console, position, game):
# Check the passed variables for validity
if not(0 <= position <= 3):
raise Exception_InvalidPath
if not (console in systems.keys()):
raise Exception_InvalidConsole
try:
#Read in all the existing shortcuts from file
xfgle_filepath = os.path.join(drive, "Resources", "xfgle.hgp")
xfgle_file_handle = open(xfgle_filepath, "r", encoding="utf-8")
lines = xfgle_file_handle.readlines()
xfgle_file_handle.close()
# Check that xfgle had the correct number of lines, if it didnt we will need to fix it.
if lines == 0:
raise IOError
prefix = getPrefixFromConsole(console)
# Bug fix: Arcade shortcuts point to the ZIP file not the zfb file
if console == 'ARCADE':
#Arcade is special as its location is embedded in the ZFB
#so we need to go get it
#Regression fix: we must pass in the zfb as that is the path
#game = game + '.zfb'
ROM_path = os.path.join(drive, console, game)
game = extractFileNameFromZFB(ROM_path)
# Overwrite the one line we want to change
lines[4*systems[console][3]+position] = f"{prefix} {game}*\n"
# Save the changes out to file
xfgle_file_handle = open(xfgle_filepath, "w", encoding="utf-8")
for line in lines:
xfgle_file_handle.write(line)
xfgle_file_handle.close()
except (OSError, IOError) as e:
logging.error(f"Tadpole_functions~changeGameShortcut: Failed changing the shortcut file. {str(e)}")
return False
return -1
# NOTE: this doesn't really work as the shortcuts persist visually so removing
# will just mean the links don't go to anything
# def deleteGameShortcut(index_path, console, position, game):
# # Check the passed variables for validity
# if not(0 <= position <= 3):
# raise Exception_InvalidPath
# if not (console in systems.keys()):
# raise Exception_InvalidConsole
# try:
# trimmedGameName = frogtool.strip_file_extension(game)
# #print(f"Filename trimmed to: {trimmedGameName}")
# #Read in all the existing shortcuts from file
# xfgle_filepath = os.path.join(index_path, "Resources", "xfgle.hgp")
# xfgle_file_handle = open(xfgle_filepath, "r")
# lines = xfgle_file_handle.readlines()
# xfgle_file_handle.close()
# prefix = getPrefixFromConsole(console)
# # Overwrite the one line we want to change
# lines[4*systems[console][3]+position] = f"{prefix} {game}*\n"
# # Save the changes out to file
# xfgle_file_handle = open(xfgle_filepath, "w")
# for line in lines:
# if line.strip("\n") != f"{prefix} {game}*":
# xfgle_file_handle.write(line)
# xfgle_file_handle.close()
# except (OSError, IOError):
# print(f"! Failed changing the shortcut file")
# return False
# return -1
#returns the position of the game's shortcut on the main screen. If it isn't a shortcut, it returns 0
def getGameShortcutPosition(drive, console, game):
try:
gamePath = os.path.join(drive, console, game)
#Read in all the existing shortcuts from file
xfgle_filepath = os.path.join(drive, "Resources", "xfgle.hgp")
xfgle_file_handle = open(xfgle_filepath, "r")
lines = xfgle_file_handle.readlines()
xfgle_file_handle.close()
prefix = getPrefixFromConsole(console)
#Arcade is special; the actual game name is embedded in the ZFB
if(console == "ARCADE" ):
game = extractFileNameFromZFB(gamePath)
savedShortcut = f"{prefix} {game}*\n"
# see if this game is listed. If so get its position
for i, gameShortcutLine in enumerate(lines):
if gameShortcutLine == savedShortcut:
print("Found " + savedShortcut + "as shortcut")
#now we found the match of the raw location, now we need to return the position from console
#from xfgle, the positions start with 3 random lines, and then go down in order from FC -> SNES -> ... -> Arcade
if(console == "FC" ):
return (i - 3)
if(console == "SFC" ):
return (i - 7)
if(console == "MD" ):
return (i - 11)
if(console == "GB" ):
return (i - 15)
if(console == "GBC" ):
return (i - 19)
if(console == "GBA" ):
return (i - 23)
if(console == "ARCADE" ):
return (i - 27)
return 0
except (OSError, IOError):
print(f"! Failed changing the shortcut file")
return 0
#Although not required, if you don't have seperate prefixes, games with same ROM names/extension
# e.g. Gameboy, gameboy color, and gameboy advance can get confused when loading the shortcuts in other systems.
def getPrefixFromConsole(console):
if console == "FC":
return 1
elif console == "SFC":
return 2
elif console == "MD":
return 3
elif console == "GB":
return 4
elif console == "GBC":
return 5
elif console == "GBA":
return 7
else:
return 6 #Aracde NEEDS 6 so always default to that
def findSequence(needle, haystack, offset = 0):
# Loop through the data array starting from the offset
for i in range(len(haystack) - offset - len(needle) + 1):
readpoint = offset + i
# Assume a match until proven otherwise
match = True
# Loop through the target sequence and compare each byte
for j in range(len(needle)):
if haystack[readpoint + j] != needle[j]:
# Mismatch found, break the inner loop and continue the outer loop
match = False
break
# If match is still true after the inner loop, we have found a match
if match:
# Return the index of the first byte of the match
return readpoint
# If we reach this point, no match was found
return -1
"""
This function is used to check if the supplied path has relevant folders and files for an SF2000 SD card.
This should be used to prevent people from accidentally overwriting their other drives.
If the correct files are found it will return True.
If the correct files are not found it will return False.
"""
def checkDriveLooksFroggy(froggypath):
bisrvpath = os.path.join(froggypath,"bios","bisrv.asd")
if os.path.exists(bisrvpath):
return True
return False
def get_background_music(url="https://api.github.com/repos/EricGoldsteinNz/SF2000_Resources/contents/BackgroundMusic"):
"""gets index of background music from provided GitHub API URL"""
music = {}
response = requests.get(url)
if response.status_code == 200:
data = json.loads(response.content)
for item in data:
music[item['name'].replace(".bgm", "")] = item['download_url']
return music
raise ConnectionError("Unable to obtain music resources. (Status Code: {})".format(response.status_code))
def get_themes(url="https://api.github.com/repos/EricGoldsteinNz/SF2000_Resources/contents/Themes") -> bool:
"""gets index of theme from provided GitHub API URL"""
theme = {}
response = requests.get(url)
if response.status_code == 200:
data = json.loads(response.content)
for item in data:
theme[item['name'].replace(".zip", "")] = item['download_url']
return theme
raise ConnectionError("Unable to obtain theme resources. (Status Code: {})".format(response.status_code))
def get_boot_logos(url="https://api.github.com/repos/EricGoldsteinNz/SF2000_Resources/contents/BootLogos") -> bool:
"""gets index of theme from provided GitHub API URL"""
bootlogos = {}
response = requests.get(url)
if response.status_code == 200:
data = json.loads(response.content)
for item in data:
bootlogos[item['name'].replace(".zip", "")] = item['download_url']
return bootlogos
raise ConnectionError("Unable to obtain boot logo resources. (Status Code: {})".format(response.status_code))
"""
This function downloads a file from the internet and renames it to pagefile.sys to replace the background music.
"""
def changeBackgroundMusic(drive_path: str, url: str = "", file: str = "") -> bool:
"""
Changes background music to music from the provided URL or file
Params:
url (str): URL to music file to use for replacement.
file (str): Full path to a local file to use for replacement.
Returns:
bool: True if successful, False if not.
Raises:
ValueError: When both url and file params are provided.
"""
if url and not file:
return downloadAndReplace(drive_path, os.path.join("Resources","pagefile.sys"), url)
elif file and not url:
try:
shutil.copyfile(file, os.path.join(drive_path, "Resources", "pagefile.sys"))
return True
except:
return False
else:
raise ValueError("Provide only url or path, not both")
"""
This function downloads a file from the internet and downloads it to resources.
"""
def changeTheme(drive_path: str, url: str = "", file: str = "", progressBar: QProgressBar = "") -> bool:
"""
Changes background theme from the provided URL or file
Params:
url (str): URL to theme files to use for replacement.
file (str): Full path to a zip file to use for replacement.
ProgressBar: address of the progressbar to update on screen
Returns:
bool: True if successful, False if not.
Raises:
ValueError: When both url and file params are provided.
"""
# TODO do this in memory instead
if url and not file:
zip_file = "theme.zip"
downloadFileFromGithub(zip_file, url)
try:
with zipfile.ZipFile(zip_file) as zip:
progressBar.setMaximum(len(zip.infolist()))
progress = 6
#TODO: Hacky but assume any zip folder with more than 55 files is not a theme zip
if len(zip.infolist()) > 55:
return False
for zip_info in zip.infolist():
#print(zip_info)
if zip_info.is_dir():
continue
zip_info.filename = os.path.basename(zip_info.filename)
progress += 1
progressBar.setValue(progress)
QApplication.processEvents()
resourcePath = os.path.join(drive_path, "Resources")
zip.extract(zip_info, resourcePath)
#Cleanup temp zip file
if os.path.exists(zip_file):
os.remove(zip_file)
return True
except:
if os.path.exists(zip_file):
os.remove(zip_file)
return False
return True
elif file and not url:
try:
with zipfile.ZipFile(file) as zip:
progressBar.setMaximum(len(zip.infolist()))
progress = 2
for zip_info in zip.infolist():
#print(zip_info)
if zip_info.is_dir():
continue
zip_info.filename = os.path.basename(zip_info.filename)
progress += 1
progressBar.setValue(progress)
QApplication.processEvents()
#TODO validate this is a real theme...maybe just check a set of files?
resourcePath = os.path.join(drive_path, "Resources")
zip.extract(zip_info, resourcePath)
return True
except:
return False
else:
raise ValueError("Error updating theme")
def changeConsoleLogos(drivePath, url=""):
return downloadAndReplace(drivePath, os.path.join("Resources","sfcdr.cpl"), url)
def downloadAndReplace(drivePath, fileToReplace, url=""):
try:
# retrieve bgm from GitHub resources
content = ""
if not url == "":
print(f"Downloading {fileToReplace} from {url}")
content = requests.get(url).content
if not content == "":
#write the content to file
bgmPath = os.path.join(drivePath, fileToReplace)
file_handle = open(bgmPath, 'wb') #rb for read, wb for write
file_handle.write(content)
file_handle.close()
print ("Finished download and replace successfully")
return True
except (OSError, IOError) as error:
print("An error occured while trying to download and replace a file.")
return False
def downloadDirectoryFromGithub(location, url, progressBar):
response = requests.get(url)
if response.status_code == 200:
data = json.loads(response.content)
downloadTotal = 0
progressBar.setMaximum(len(data)+1)
for item in data:
if item["type"] == "dir":
#create folder then recursively download
foldername = item["name"]
destination = os.path.join(location,foldername)
print(f"creating directory if it doesnt exist {destination}")
os.makedirs(destination, exist_ok=True)
downloadDirectoryFromGithub(destination, item["url"], progressBar)
else:# all other cases should be files
filename = item["name"]
downloadFileFromGithub(os.path.join(location,filename), item["download_url"])
downloadTotal += 1
progressBar.setValue(downloadTotal)
QApplication.processEvents()
return True
raise ConnectionError("Unable to V1.5 Update. (Status Code: {})".format(response.status_code))
return False
def downloadFileFromGithub(outFile, url):
try:
response = requests.get(url)
if response.status_code == 200:
with open(outFile, 'wb') as f:
print(f'downloading {url} to {outFile}')
f.write(response.content)
return True
else:
print("Error when trying to download a file from Github. Response was not code 200")
raise InvalidURLError
except Exception as e:
print(str(e))
return False
"""
#This function has been replaced by "downloadAndExtractZIPBar"
def downloadAndExtractZIP(root, url, progress):
try:
response = requests.get(url)
logging.info(f"tadpole_functions~downloadAndExtractZIP: Received {response.status_code} for ({url}) with length {len(response.content)}")
if response.status_code == 200:
progress.showProgress(50, True)
zip = zipfile.ZipFile(BytesIO(response.content))
zip.extractall(path=root)
return True
else:
logging.error("tadpole_functions~downloadAndExtractZIP: Problem when trying to download a file from Github. Response was not code 200")
raise InvalidURLError
except Exception as e:
logging.error(f"tadpole_functions~downloadAndExtractZIP: ERROR {str(e)}")
return False
"""
def downloadAndExtractZIPBar(root, url, progress):
try:
logging.info(f"tadpole_functions~downloadAndExtractZIPBar: Downloading ({url}) to extract to ({root})")
response = requests.get(url, stream=True)
total_length = int(response.headers.get('content-length'))
dl = 0
zip_in_memory = bytearray()
for data in response.iter_content(chunk_size=4096):
if data:
dl += len(data)
zip_in_memory.extend(data)
progress.showProgress(int(100 * dl / total_length), True)
logging.info(f"tadpole_functions~downloadAndExtractZIPBar: Received {response.status_code} for ({url})")
if response.status_code == 200:
progress.setText("Extracting")
progress.showProgress(0, True)
zip = zipfile.ZipFile(BytesIO(zip_in_memory))
zip.extractall(path=root)
progress.showProgress(100, True)
return True
else:
logging.error("tadpole_functions~downloadAndExtractZIPBar: Problem when trying to download a file from Github. Response was not code 200")
raise InvalidURLError
except Exception as e:
logging.error(f"tadpole_functions~downloadAndExtractZIPBar: ERROR {str(e)}")
return False
#Keeping this for now as a failsafe, but should remove it to follow the new design structure
def DownloadOSFiles(correct_drive, progress):
downloadDirectoryFromGithub(correct_drive,"https://api.github.com/repos/EricGoldsteinNz/SF2000_Resources/contents/OS/V1.6", progress)
#Make the ROM directories
os.mkdir(os.path.join(correct_drive,"ARCADE"))
os.mkdir(os.path.join(correct_drive,"ARCADE","bin"))
os.mkdir(os.path.join(correct_drive,"ARCADE","save"))
os.mkdir(os.path.join(correct_drive,"ARCADE","skp"))
os.mkdir(os.path.join(correct_drive,"FC"))
os.mkdir(os.path.join(correct_drive,"FC","save"))
os.mkdir(os.path.join(correct_drive,"GB"))
os.mkdir(os.path.join(correct_drive,"GB","save"))
os.mkdir(os.path.join(correct_drive,"GBC"))
os.mkdir(os.path.join(correct_drive,"GBC","save"))
os.mkdir(os.path.join(correct_drive,"GBA"))
os.mkdir(os.path.join(correct_drive,"GBA","save"))
os.mkdir(os.path.join(correct_drive,"MD"))
os.mkdir(os.path.join(correct_drive,"MD","save"))
os.mkdir(os.path.join(correct_drive,"SFC"))
os.mkdir(os.path.join(correct_drive,"SFC","save"))
os.mkdir(os.path.join(correct_drive,"ROMS"))
os.mkdir(os.path.join(correct_drive,"ROMS","save"))
#Need to delete bisrv.asd again to prevent bootloader bug
if os.path.exists(os.path.join(correct_drive,"bios","bisrv.asd")):
os.remove(os.path.join(correct_drive,"bios","bisrv.asd"))
#Re-add biserv.asd
#TODO: Review why we are doing this
#Jason: Per Dteyn, we need to remove and redownlaod bisrv.asd to clear the known bug bootloader crash
downloadFileFromGithub(os.path.join(correct_drive,"bios","bisrv.asd"), "https://raw.githubusercontent.com/EricGoldsteinNz/SF2000_Resources/main/OS/V1.6/bios/bisrv.asd")
return True
def emptyFavourites(drive) -> bool:
return emptyFile(os.path.join(drive, "Resources", "Favorites.bin"))
def emptyFile(path) -> bool:
print(f"Deleting file {path}")
try:
if os.path.isfile(path):
os.remove(path)
else:
print("File not found, guess thats still a success? @Goldstein to check the spelling if this is a bug")
return True
except:
print("Error while trying to delete a file")
return False
def emptyHistory(drive) -> bool:
return emptyFile(os.path.join(drive, "Resources", "History.bin"))
#Thanks Von Millhausen
#The first 59,905 bytes are for the thumbnail image (208px * 144px, stored in RGB565 format which is two bytes per pixel),
# then there's four null bytes, then there's the name of a .zip file with no path (presumably /ARCADE/bin/ is hardcoded), and then finally two null bytes
def extractFileNameFromZFB(romFilePath):
try:
with open(romFilePath, "rb") as rom_file:
#get past the RAW image
rom_file.seek(59908)
#need to now go until we find the null bytes
rom_file_end = rom_file.read()
rom_name_content = bytearray()
i = 0
while i < len(rom_file_end):
if rom_file_end[i] == 0x00 and rom_file_end[i+1] == 0x00:
break
rom_name_content.append(rom_file_end[i])
i += 1
#rom_content = bytearray(rom_file.read(907))
fileName = rom_name_content.decode()
logging.info(f"({fileName}) decoded from ZFB")
return fileName
except Exception as e:
logging.error(f"tadpole_functions~extractFileNameFromZFB: error {str(e)}")
return ''
#Thanks DTeyn for the code!: https://github.com/Dteyn/ZFBTool/blob/master/ZFBTool.pyw
def createZFBFile(drive, pngPath, romPath):
"""Creates a .ZFB file with input .PNG file and ARCADE ROM .ZIP name"""
# Define the size of the thumbnail
thumb_size = (144, 208)
try:
#if its blank, just give it 1's as raw data for the first bytes
if pngPath == '':
raw_data_bytes = bytes(b'\x01' * 59904)
else:
with Image.open(pngPath) as img:
img = img.resize(thumb_size)
img = img.convert("RGB")
raw_data = []
# Convert image to RGB565
for y in range(thumb_size[1]):
for x in range(thumb_size[0]):
r, g, b = img.getpixel((x, y))
rgb = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
raw_data.append(struct.pack('H', rgb))
raw_data_bytes = b''.join(raw_data)
# Create .zfb filename
ZIPName = os.path.basename(romPath)
ROMName = os.path.splitext(ZIPName)[0]
zfb_file = os.path.join(drive, 'ARCADE', ROMName + '.zfb')
# Now we write the entire ZFB file
with open(zfb_file, 'wb') as zfb:
# Write the image data to the .zfb file
zfb.write(raw_data_bytes)
# Write four 00 bytes
zfb.write(b'\x00\x00\x00\x00')
# Write the ROM filename
zfb.write(ZIPName.encode())
# Write two 00 bytes
zfb.write(b'\x00\x00')
logging.info(f"ZFB file created successfully.")
return True
except Exception as e:
logging.error(f"An error occurred while creating the ZFB file: {str(e)}")
return False
def deleteROM(ROMfilePath):
logging.info(f"Tadpole_functions~ Deleting ROM: {ROMfilePath}")
ext = os.path.splitext(ROMfilePath)[1]
if(ext == ".zfb"): #ROM is arcade, need to also delete the zip file in bin
print("Arcade ROM")
base = os.path.dirname(ROMfilePath)
arcadezip = extractFileNameFromZFB(ROMfilePath)
try:
os.remove(os.path.join(base,"bin",arcadezip))
except:
logging.error(f"ERROR: tadpole_functions~deleteROM: failed to delete arcadezip ({ROMfilePath})")
# We dont return False here because the main point is to delete the provided ROMfilePath file
# Delete the zxx file
try: