1import hashlib 2import os 3from binascii import hexlify 4 5from fsbc import settings 6from fsgs.drivers.mednafendriver import MednafenDriver 7from fsgs.drivers.messdriver import MessDriver 8from fsgs.drivers.retroarchdriver import RetroArchDriver 9from fsgs.option import Option 10from fsgs.platform import Platform 11from fsgs.platforms.loader import SimpleLoader 12 13SMD_MODEL_NTSC = "ntsc" 14SMD_MODEL_NTSC_J = "ntsc-j" 15SMD_MODEL_PAL = "pal" 16SMD_CONTROLLER_TYPE = "gamepad" 17SMD_CONTROLLER = { 18 "type": SMD_CONTROLLER_TYPE, 19 "description": "Gamepad", 20 "mapping_name": "megadrive", 21} 22SMD_6BUTTON_CONTROLLER_TYPE = "gamepad6" 23SMD_6BUTTON_CONTROLLER = { 24 "type": SMD_6BUTTON_CONTROLLER_TYPE, 25 "description": "Gamepad", 26 "mapping_name": "megadrive", 27} 28NO_CONTROLLER_TYPE = "none" 29NO_CONTROLLER = { 30 "type": NO_CONTROLLER_TYPE, 31 "description": "None", 32 "mapping_name": "", 33} 34SMD_PORTS = [ 35 { 36 "description": "Port 1", 37 "types": [SMD_CONTROLLER, SMD_6BUTTON_CONTROLLER, NO_CONTROLLER], 38 "type_option": "smd_port_1_type", 39 "device_option": "smd_port_1", 40 }, 41 { 42 "description": "Port 2", 43 "types": [SMD_CONTROLLER, SMD_6BUTTON_CONTROLLER, NO_CONTROLLER], 44 "type_option": "smd_port_2_type", 45 "device_option": "smd_port_2", 46 }, 47] 48 49 50class MegaDrivePlatform(Platform): 51 PLATFORM_NAME = "Mega Drive" 52 53 def driver(self, fsgc): 54 emulator = settings.get(Option.SMD_EMULATOR) 55 56 if emulator in ["retroarch-fs", "retroarch-fs/genesisplusgx"]: 57 return MegaDriveRetroArchDriver(fsgc) 58 if emulator in ["mame-fs"]: 59 return MegaDriveMameDriver(fsgc) 60 if emulator in ["mednafen"]: 61 return MegaDriveMednafenDriver(fsgc) 62 63 # FIXME: Vanilla retroarch not supported yet 64 if emulator in ["retroarch", "retroarch/genesisplusgx"]: 65 return MegaDriveRetroArchDriver(fsgc) 66 # Deprecated name 67 if emulator in ["retroarch-genesis-plus-gx"]: 68 return MegaDriveRetroArchDriver(fsgc) 69 # Legacy names 70 if emulator in ["mame", "mess"]: 71 return MegaDriveMameDriver(fsgc) 72 73 return MegaDriveMednafenFSDriver(fsgc) 74 75 def loader(self, fsgc): 76 return MegaDriveLoader(fsgc) 77 78 79class MegaDriveLoader(SimpleLoader): 80 def load(self, values): 81 super().load(values) 82 self.config[Option.SMD_MODEL] = values["smd_model"] 83 self.config[Option.SMD_PORT_1_TYPE] = values["smd_port_1_type"] 84 self.config[Option.SMD_PORT_2_TYPE] = values["smd_port_2_type"] 85 # self.config[Option.SMD_PORT_3_TYPE] = values["smd_port_3_type"] 86 # self.config[Option.SMD_PORT_4_TYPE] = values["smd_port_4_type"] 87 88 # FIXME: Should be able to remove this now (smd_model in database) 89 if not self.config[Option.SMD_MODEL]: 90 variant = values["variant_name"].lower() 91 if "world" in variant or "usa" in variant: 92 model = SMD_MODEL_NTSC 93 elif "europe" in variant or "australia" in variant: 94 model = SMD_MODEL_PAL 95 elif "japan" in variant: 96 model = SMD_MODEL_NTSC_J 97 else: 98 # FIXME: Remove? 99 # model = SMD_MODEL_AUTO 100 model = SMD_MODEL_NTSC 101 self.config[Option.SMD_MODEL] = model 102 103 104class MegaDriveMameDriver(MessDriver): 105 PORTS = SMD_PORTS 106 107 def __init__(self, fsgc): 108 super().__init__(fsgc) 109 self.helper = MegaDriveHelper(self.options) 110 self.save_handler.set_save_data_is_emulator_specific(True) 111 self.save_handler.set_mame_driver(self.get_mame_driver()) 112 113 def prepare(self): 114 print("[SMD] MAME driver preparing...") 115 super().prepare() 116 self.emulator.args.extend(["-cart", self.helper.prepare_rom(self)]) 117 118 def get_mame_driver(self): 119 self.helper.set_model_name_from_model(self) 120 if self.helper.model() == SMD_MODEL_NTSC: 121 return "genesis" 122 elif self.helper.model() == SMD_MODEL_PAL: 123 return "megadriv" 124 elif self.helper.model() == SMD_MODEL_NTSC_J: 125 return "megadrij" 126 raise Exception("Could not determine SMD MAME driver") 127 128 def get_mess_input_mapping(self, _): 129 return { 130 "START": "P#_START", 131 "MODE": "P#_SELECT", 132 "UP": "P#_JOYSTICK_UP", 133 "DOWN": "P#_JOYSTICK_DOWN", 134 "LEFT": "P#_JOYSTICK_LEFT", 135 "RIGHT": "P#_JOYSTICK_RIGHT", 136 "A": "P#_BUTTON1", 137 "B": "P#_BUTTON2", 138 "C": "P#_BUTTON3", 139 "X": "P#_BUTTON4", 140 "Y": "P#_BUTTON5", 141 "Z": "P#_BUTTON6", 142 } 143 144 def get_mess_romset(self): 145 return self.get_mame_driver(), {} 146 147 148class MegaDriveMednafenDriver(MednafenDriver): 149 PORTS = SMD_PORTS 150 151 def __init__(self, fsgc, vanilla=True): 152 super().__init__(fsgc, vanilla) 153 self.helper = MegaDriveHelper(self.options) 154 self.save_handler.set_save_data_is_emulator_specific(True) 155 # self.save_handler.set_srm_alias(".sav") 156 157 def prepare(self): 158 print("[SMD] Mednafen driver preparing...") 159 super().prepare() 160 rom_path = self.helper.prepare_rom(self) 161 self.helper.read_rom_metadata(rom_path) 162 self.init_mega_drive_model() 163 for i in range(len(self.PORTS)): 164 self.init_mega_drive_port(i) 165 self.init_mednafen_crop_from_viewport() 166 self.set_mednafen_aspect(4, 3) 167 # We do aspect calculation separately. Must not be done twice. 168 self.emulator.args.extend(["-md.correct_aspect", "0"]) 169 # ROM path must be added at the end of the argument list 170 self.emulator.args.append(rom_path) 171 172 def init_mega_drive_model(self): 173 self.helper.set_model_name_from_model(self) 174 if self.helper.model() == SMD_MODEL_NTSC: 175 region = "overseas_ntsc" 176 elif self.helper.model() == SMD_MODEL_PAL: 177 region = "overseas_pal" 178 elif self.helper.model() == SMD_MODEL_NTSC_J: 179 region = "domestic_ntsc" 180 else: 181 # FIXME: This model might disappear 182 region = "game" 183 self.emulator.args.extend(["-md.region", region]) 184 185 def init_mega_drive_port(self, i): 186 t = self.ports[i].type 187 n = i + 1 188 if t == SMD_CONTROLLER_TYPE: 189 t = "gamepad" 190 elif t == SMD_6BUTTON_CONTROLLER_TYPE: 191 t = "gamepad6" 192 elif t == NO_CONTROLLER_TYPE: 193 t = "none" 194 else: 195 self.logger.warning( 196 "Unknown port type '{}' for Mega Drive port {}".format(t, n) 197 ) 198 t = "gamepad" 199 self.emulator.args.extend(["-md.input.port{}".format(n), t]) 200 201 def init_mednafen_crop_from_viewport(self): 202 viewport = self.options[Option.VIEWPORT] 203 if viewport: 204 if viewport.startswith("0 0 320 240 ="): 205 viewport_src, viewport = self.mednafen_viewport() 206 self.emulator.env["FSGS_CROP"] = "{},{},{},{}".format( 207 viewport[0], viewport[1], viewport[2], viewport[3] 208 ) 209 else: 210 if self.scaling == self.NO_SCALING: 211 if self.helper.model() == SMD_MODEL_PAL: 212 self.emulator.env["FSGS_CROP"] = "0,0,320,240" 213 else: 214 self.emulator.env["FSGS_CROP"] = "0,8,320,224" 215 216 def mednafen_system_prefix(self): 217 return "md" 218 219 def mednafen_input_mapping(self, port): 220 n = port + 1 221 return { 222 "A": "md.input.port{}.gamepad.a".format(n), 223 "B": "md.input.port{}.gamepad.b".format(n), 224 "C": "md.input.port{}.gamepad.c".format(n), 225 "X": "md.input.port{}.gamepad.x".format(n), 226 "Y": "md.input.port{}.gamepad.y".format(n), 227 "Z": "md.input.port{}.gamepad.z".format(n), 228 "UP": "md.input.port{}.gamepad.up".format(n), 229 "DOWN": "md.input.port{}.gamepad.down".format(n), 230 "LEFT": "md.input.port{}.gamepad.left".format(n), 231 "RIGHT": "md.input.port{}.gamepad.right".format(n), 232 "MODE": "md.input.port{}.gamepad.mode".format(n), 233 "START": "md.input.port{}.gamepad.start".format(n), 234 } 235 236 # FIXME: add PAUSE button to universal gamepad config 237 238 # def game_video_par(self): 239 # # These may not be entirely correct... 240 # if self.is_pal(): 241 # return (4 / 3) / (320 / 240) 242 # else: 243 # return (4 / 3) / (320 / 224) 244 245 def game_video_size(self): 246 viewport_src, viewport = self.mednafen_viewport() 247 if viewport is not None: 248 return viewport[2], viewport[3] 249 if self.helper.model() == SMD_MODEL_PAL: 250 return 320, 240 251 else: 252 return 320, 224 253 254 def get_game_file(self, config_key="cartridge_slot"): 255 return None 256 257 258class MegaDriveMednafenFSDriver(MegaDriveMednafenDriver): 259 def __init__(self, fsgs): 260 super().__init__(fsgs, vanilla=False) 261 262 263class MegaDriveRetroArchDriver(RetroArchDriver): 264 PORTS = SMD_PORTS 265 266 def __init__(self, fsgc): 267 super().__init__( 268 fsgc, "genesis_plus_gx_libretro", "RetroArch/GenesisPlusGX" 269 ) 270 self.helper = MegaDriveHelper(self.options) 271 self.save_handler.set_save_data_is_emulator_specific(True) 272 273 def prepare(self): 274 print("[SMD] RetroArch/GenesisPlusGX driver preparing...") 275 super().prepare() 276 hw = "mega drive / genesis" 277 region = self.init_mega_drive_model() 278 for i in range(len(self.PORTS)): 279 self.init_mega_drive_port(i) 280 with self.open_retroarch_core_options() as f: 281 f.write('genesis_plus_gx_system_hw = "{}"\n'.format(hw)) 282 f.write("genesis_plus_gx_region_detect = {}\n".format(region)) 283 self.emulator.args.append(self.helper.prepare_rom(self)) 284 285 def init_mega_drive_model(self): 286 self.helper.set_model_name_from_model(self) 287 if self.helper.model() == SMD_MODEL_NTSC: 288 return "ntsc-u" 289 elif self.helper.model() == SMD_MODEL_PAL: 290 return "pal" 291 elif self.helper.model() == SMD_MODEL_NTSC_J: 292 return "ntsc-j" 293 return "ntsc-u" 294 295 def init_mega_drive_port(self, i): 296 t = self.ports[i].type 297 n = i + 1 298 if t == SMD_CONTROLLER_TYPE: 299 t = "257" 300 elif t == SMD_6BUTTON_CONTROLLER_TYPE: 301 t = "513" 302 elif t == NO_CONTROLLER_TYPE: 303 t = "0" 304 else: 305 self.logger.warning( 306 "Unknown port type '{}' for Mega Drive port {}".format(t, n) 307 ) 308 t = "gamepad" 309 # FIXME: Note, this only works for p1 and p2. Port 3 and beyond 310 # supports RetroPad only? 311 self.retroarch_config["input_libretro_device_p{}".format(n)] = t 312 313 def game_video_size(self): 314 # # FIXME: Account for horizontal overscan 315 # # FIXME: Account for vertical overscan 316 # 317 # viewport = self.options[Option.VIEWPORT] 318 # if viewport == "0 0 256 240 = 0 0 256 240": 319 # return 256, 240 320 # # elif viewport == "0 0 256 240 = 0 8 256 224": 321 # # return 256, 224 322 # elif viewport == "0 0 256 240 = 8 0 240 240": 323 # return 240, 240 324 # elif viewport == "0 0 256 240 = 8 8 240 224": 325 # return 240, 224 326 # else: 327 # return 256, 224 328 329 # FIXME: PAL? 330 return 320, 224 331 332 def retroarch_input_mapping(self, port): 333 n = port + 1 334 return { 335 "A": "input_player{}_y".format(n), 336 "B": "input_player{}_b".format(n), 337 "C": "input_player{}_a".format(n), 338 "X": "input_player{}_l".format(n), 339 "Y": "input_player{}_x".format(n), 340 "Z": "input_player{}_r".format(n), 341 "UP": "input_player{}_up".format(n), 342 "DOWN": "input_player{}_down".format(n), 343 "LEFT": "input_player{}_left".format(n), 344 "RIGHT": "input_player{}_right".format(n), 345 "MODE": "input_player{}_select".format(n), 346 "START": "input_player{}_start".format(n), 347 } 348 349 # def window_size(self): 350 # return 256, 224 351 352 353class MegaDriveHelper: 354 def __init__(self, options): 355 self.options = options 356 357 def model(self): 358 if self.options[Option.SMD_MODEL] == SMD_MODEL_NTSC: 359 return SMD_MODEL_NTSC 360 if self.options[Option.SMD_MODEL] == SMD_MODEL_NTSC_J: 361 return SMD_MODEL_NTSC_J 362 if self.options[Option.SMD_MODEL] == SMD_MODEL_PAL: 363 return SMD_MODEL_PAL 364 # FIXME: REMOVE? 365 # return SMD_MODEL_AUTO 366 return SMD_MODEL_NTSC 367 368 def set_model_name_from_model(self, driver): 369 model = self.model() 370 if model == SMD_MODEL_NTSC: 371 driver.set_model_name("Genesis") 372 elif model == SMD_MODEL_PAL: 373 driver.set_model_name("Mega Drive PAL") 374 elif model == SMD_MODEL_NTSC_J: 375 driver.set_model_name("Mega Drive NTSC-J") 376 # else: 377 # # FIXME: This model might disappear 378 # # driver.set_model_name("Mega Drive / Genesis") 379 # assert False 380 381 def read_rom_metadata(self, rom_path): 382 # FIXME: Move to MegaDriveHeaderParser 383 # http://md.squee.co/Howto:Initialise_a_Mega_Drive 384 # https://en.wikibooks.org/wiki/Genesis_Programming 385 with open(rom_path, "rb") as f: 386 data = f.read(0x200) 387 if not len(data) == 0x200: 388 print("[SMD] Did not read 0x200 header bytes") 389 return 390 region = data[0x200 - 16 :] 391 # notes = data[0x200 - 16 - 52] 392 sram_end = data[0x200 - 16 - 52 - 4 * 1 : 0x200 - 16 - 52 - 4 * 0] 393 sram_start = data[0x200 - 16 - 52 - 4 * 2 : 0x200 - 16 - 52 - 4 * 1] 394 sram_type = data[0x200 - 16 - 52 - 4 * 3 : 0x200 - 16 - 52 - 4 * 2] 395 396 sram_end = hexlify(sram_end) 397 sram_start = hexlify(sram_start) 398 # sram_type = hexlify(sram_type) 399 sram_start = int(sram_start, 16) 400 sram_end = int(sram_end, 16) 401 print("[SMD] Region:", repr(region)) 402 print("[SMD] SRAM Type :", repr(sram_type)) 403 if sram_type[0] == 0x52: 404 print(" - 0x52 OK") 405 else: 406 print(" - 0x52 MISSING") 407 if sram_type[1] == 0x41: 408 print(" - 0x41 OK") 409 else: 410 print(" - 0x41 MISSING") 411 is_backup = (sram_type[2] & 0b01000000) != 0 412 odd, even = False, False 413 if (sram_type[2] & 0b00011000) == 0b00011000: 414 odd = True 415 elif (sram_type[2] & 0b00011000) == 0b00010000: 416 even = True 417 elif (sram_type[2] & 0b00011000) == 0b00000000: 418 even = True 419 odd = True 420 print(" - Is backup RAM?", is_backup) 421 print(" - Odd?", odd, "Even?", even) 422 print("[SMD] SRAM Start:", repr(sram_start)) 423 print("[SMD] SRAM End :", repr(sram_end)) 424 425 # FIXME: Shared, move into common module (find all occurrences) 426 def prepare_rom(self, driver): 427 file_uri = self.options[Option.CARTRIDGE_SLOT] 428 input_stream = driver.fsgc.file.open(file_uri) 429 _, ext = os.path.splitext(file_uri) 430 return self.prepare_rom_with_stream(driver, input_stream, ext) 431 432 # FIXME: Shared, move into common module (find all occurrences) 433 def prepare_rom_with_stream(self, driver, input_stream, ext): 434 sha1_obj = hashlib.sha1() 435 path = driver.temp_file("rom" + ext).path 436 with open(path, "wb") as f: 437 while True: 438 data = input_stream.read(65536) 439 if not data: 440 break 441 f.write(data) 442 sha1_obj.update(data) 443 new_path = os.path.join( 444 os.path.dirname(path), sha1_obj.hexdigest()[:8].upper() + ext 445 ) 446 os.rename(path, new_path) 447 return new_path 448 449 450class MegaDriveHeaderParser: 451 def __init__(self): 452 self.sram = False 453 self.sram_start = 0 454 self.sram_end = 0 455 self.sram_size = 0 456 self.sram_odd_only = False 457 self.sram_even_only = False 458 self.sram_odd_and_even = False 459 460 def parse_file(self, path): 461 with open(path, "rb") as f: 462 data = f.read(0x200) 463 if not len(data) == 0x200: 464 return False 465 return self.parse_data(data) 466 467 def parse_data(self, data): 468 if not len(data) >= 0x200: 469 return False 470