1import hashlib 2import os 3 4from fsgs import Option 5from fsgs.drivers.mednafendriver import MednafenDriver 6from fsgs.platform import Platform 7from fsgs.platforms.loader import SimpleLoader 8 9# FIXME: Use KnownFile 10# [BIOS] Game Boy Advance (World).gba 11GBA_WORLD_BIOS_SHA1 = "300c20df6731a33952ded8c436f7f186d25d3492" 12 13GBA_CONTROLLER = { 14 "type": "gamepad", 15 "description": "Built-in Controller", 16 "mapping_name": "gameboyadvance", 17} 18GBA_PORTS = [ 19 { 20 "description": "Built-in", 21 "types": [GBA_CONTROLLER], 22 "type_option": "gba_port_1_type", 23 "device_option": "gba_port_1", 24 } 25] 26 27 28class GameBoyAdvancePlatform(Platform): 29 PLATFORM_NAME = "Game Boy Advance" 30 31 def driver(self, fsgc): 32 return GameBoyAdvanceMednafenDriver(fsgc) 33 34 def loader(self, fsgc): 35 return GameBoyAdvanceLoader(fsgc) 36 37 38class GameBoyAdvanceLoader(SimpleLoader): 39 def load_extra(self, values): 40 self.config[Option.SRAM_TYPE] = values["sram_type"] 41 42 43class GameBoyAdvanceMednafenDriver(MednafenDriver): 44 PORTS = GBA_PORTS 45 46 def __init__(self, fsgc): 47 super().__init__(fsgc) 48 self.helper = GameBoyAdvanceHelper(self.options) 49 self.sram_type_file = None 50 51 def prepare(self): 52 print("[GBA] Mednafen GBA driver preparing...") 53 super().prepare() 54 self.set_mednafen_aspect(3, 2) 55 # We do aspect calculation separately. Must not be done twice. 56 # self.emulator.args.extend(["-snes.correct_aspect", "0"]) 57 # FIXME: Input ports configuration 58 # FIXME: SNES model 59 rom_path = self.helper.prepare_rom(self) 60 self.emulator.args.append(rom_path) 61 62 self.configure_gba_sram(rom_path) 63 self.configure_gba_colormap() 64 self.configure_gba_bios() 65 66 print("[FIXME: temporarily removed support for custom sav file") 67 # sav_file = os.path.splitext(rom_file)[0] + u".sav" 68 # if os.path.exists(sav_file): 69 # m = hashlib.md5() 70 # with open(rom_file, "rb") as f: 71 # while True: 72 # data = f.read(16384) 73 # if not data: 74 # break 75 # m.update(data) 76 # md5sum = str(m.hexdigest()) 77 # save_name = os.path.splitext( 78 # os.path.basename(rom_file))[0] + u"." + md5sum + u".sav" 79 # dest_path = os.path.join(self.get_state_dir(), save_name) 80 # if not os.path.exists(dest_path): 81 # shutil.copy(sav_file, dest_path) 82 83 # def init_input(self): 84 # # self.inputs = [ 85 # # self.create_input(name="Controller", 86 # # type="gameboyadvance", 87 # # description="Built-in Gamepad"), 88 # # ] 89 # self.set_mednafen_input_order() 90 91 def finish(self): 92 if self.sram_type_file: 93 try: 94 os.remove(self.sram_type_file) 95 except Exception: 96 # Not expected to happen, but is not critical if it does. 97 print("[GBA] Could not remove SRAM type file") 98 super().finish() 99 100 def get_game_refresh_rate(self): 101 # all GBA games should use a refresh rate of approx. 60.0 Hz 102 # (or 30.0 Hz) 103 return 59.73 104 105 def mednafen_input_mapping(self, _): 106 return { 107 "A": "gba.input.builtin.gamepad.a", 108 "B": "gba.input.builtin.gamepad.b", 109 "L": "gba.input.builtin.gamepad.shoulder_l", 110 "R": "gba.input.builtin.gamepad.shoulder_r", 111 "UP": "gba.input.builtin.gamepad.up", 112 "DOWN": "gba.input.builtin.gamepad.down", 113 "LEFT": "gba.input.builtin.gamepad.left", 114 "RIGHT": "gba.input.builtin.gamepad.right", 115 "SELECT": "gba.input.builtin.gamepad.select", 116 "START": "gba.input.builtin.gamepad.start", 117 } 118 119 def mednafen_rom_extensions(self): 120 return [".gba"] 121 122 def mednafen_scanlines_setting(self): 123 return 33 124 125 def mednafen_special_filter(self): 126 return "nn2x" 127 128 def mednafen_system_prefix(self): 129 return "gba" 130 131 def game_video_par(self): 132 return 1.0 133 134 def game_video_size(self): 135 return 240, 160 136 137 def configure_gba_sram(self, rom_path): 138 if self.options[Option.SRAM_TYPE]: 139 save_dir = self.save_handler.save_dir() 140 rom_name = os.path.splitext(os.path.basename(rom_path))[0] 141 type_file = os.path.join(save_dir, rom_name + ".type") 142 with open(type_file, "wb") as f: 143 for line in self.options[Option.SRAM_TYPE].split(";"): 144 f.write((line.strip() + "\n").encode("UTF-8")) 145 self.sram_type_file = type_file 146 147 def configure_gba_colormap(self): 148 gamma = 1.3 149 if self.options[Option.GBA_GAMMA]: 150 try: 151 gamma = float(self.options[Option.GBA_GAMMA]) 152 except ValueError: 153 print("[GBA] WARNING: Invalid gamma value") 154 self.create_colormap(os.path.join(self.home.path, "gba.pal"), gamma) 155 # if os.path.exists(os.path.join(self.home.path, "gba.pal")): 156 # os.remove(os.path.join(self.home.path, "gba.pal")) 157 # self.colormap_temp = self.temp_file("color.map") 158 # self.create_colormap(self.colormap_temp.path, gamma) 159 # self.emulator.args.extend( 160 # ["-gba.colormap", self.colormap_temp.path]) 161 162 def configure_gba_bios(self): 163 uri = self.fsgc.file.find_by_sha1(GBA_WORLD_BIOS_SHA1) 164 stream = self.fsgc.file.open(uri) 165 if stream is not None: 166 bios_temp = self.temp_file("gba_bios.bin") 167 with open(bios_temp.path, "wb") as f: 168 f.write(stream.read()) 169 self.emulator.args.extend(["-gba.bios", bios_temp.path]) 170 else: 171 print("[GBA] WARNING: GBA BIOS not found, using HLE") 172 173 @staticmethod 174 def create_colormap(path, gamma): 175 with open(path, "wb") as f: 176 for x in range(32768): 177 r = (x & 0x1F) << 3 178 g = ((x & 0x3E0) >> 5) << 3 179 b = ((x & 0x7C00) >> 10) << 3 180 r /= 255.0 181 g /= 255.0 182 b /= 255.0 183 # h, l, s = colorsys.rgb_to_hls(r, g, b) 184 # l = l ** gamma 185 # r, g, b = colorsys.hls_to_rgb(h, l, s) 186 r = r ** gamma 187 g = g ** gamma 188 b = b ** gamma 189 f.write(bytes([int(r * 255)])) 190 f.write(bytes([int(g * 255)])) 191 f.write(bytes([int(b * 255)])) 192 193 def get_game_file(self, config_key="cartridge_slot"): 194 return None 195 196 197class GameBoyAdvanceHelper: 198 def __init__(self, options): 199 self.options = options 200 201 def prepare_rom(self, driver): 202 file_uri = self.options[Option.CARTRIDGE_SLOT] 203 input_stream = driver.fsgc.file.open(file_uri) 204 _, ext = os.path.splitext(file_uri) 205 return self.prepare_rom_with_stream(driver, input_stream, ext) 206 207 def prepare_rom_with_stream(self, driver, input_stream, ext): 208 sha1_obj = hashlib.sha1() 209 path = driver.temp_file("rom" + ext).path 210 with open(path, "wb") as f: 211 while True: 212 data = input_stream.read(65536) 213 if not data: 214 break 215 f.write(data) 216 sha1_obj.update(data) 217 new_path = os.path.join( 218 os.path.dirname(path), sha1_obj.hexdigest()[:8].upper() + ext 219 ) 220 os.rename(path, new_path) 221 return new_path 222