1from binascii import hexlify 2import hashlib 3import json 4import os 5import struct 6from base64 import b64decode 7from functools import lru_cache 8 9import shutil 10 11from fsbc.paths import Paths 12from fsbc.system import System 13from fsgs.archive import Archive 14from fsgs.drivers.gamedriver import GameDriver, Emulator 15from fsgs.input.enumeratehelper import EnumerateHelper 16from fsgs.input.mapper import InputMapper 17from fsgs.option import Option 18from fsgs.saves import SaveHandler 19 20 21class MednafenDriver(GameDriver): 22 def __init__(self, fsgc, vanilla=False): 23 super().__init__(fsgc) 24 if vanilla: 25 self.emulator = Emulator("mednafen") 26 else: 27 self.emulator = Emulator("mednafen-fs") 28 self.save_handler = MednafenSaveHandler( 29 self.fsgc, options=self.options 30 ) 31 # self._game_files_added = False 32 33 def prepare(self): 34 if os.path.exists(os.path.join(self.home.path, ".mednafen")): 35 shutil.rmtree(os.path.join(self.home.path, ".mednafen")) 36 self.save_handler.prepare() 37 # self.temp_home = self.create_temp_dir("mednafen-home") 38 with open(self.mednafen_cfg_path(), "w", encoding="UTF-8") as f: 39 self.mednafen_configure(f) 40 # if not self._game_files_added: 41 game_file = self.get_game_file() 42 if game_file is not None: 43 self.emulator.args.append(game_file) 44 45 def finish(self): 46 self.save_handler.finish() 47 48 def set_mednafen_aspect(self, h, v): 49 if self.stretching() == self.NO_STRETCHING: 50 h, v = self.game_video_size() 51 self.emulator.env["FSGS_ASPECT"] = "{}/{}".format(h, v) 52 53 # FIXME: REPLACE BAD IMPLEMENTATION OF prepare cd images 54 55 @staticmethod 56 def expand_default_path(src, default_dir): 57 if "://" in src: 58 return src, None 59 src = Paths.expand_path(src, default_dir) 60 archive = Archive(src) 61 return src, archive 62 63 def prepare_mednafen_cd_images(self): 64 # self._game_files_added = True 65 66 temp_dir = self.temp_dir("media").path 67 game_file = None 68 # cdrom_drive_0 = self.config.get("cdrom_drive_0", "") 69 # if cdrom_drive_0.startswith("game:"): 70 if True: 71 # scheme, dummy, game_uuid, name = cdrom_drive_0.split("/") 72 # file_list = self.get_file_list_for_game_uuid(game_uuid) 73 file_list = json.loads(self.options[Option.FILE_LIST]) 74 for file_item in file_list: 75 src = self.fsgc.file.find_by_sha1(file_item["sha1"]) 76 77 src, archive = self.expand_default_path(src, None) 78 dst_name = file_item["name"] 79 # current_task.set_progress(dst_name) 80 81 dst = os.path.join(temp_dir, dst_name) 82 self.fsgc.file.copy_game_file(src, dst) 83 84 # cue_sheets = self.get_cue_sheets_for_game_uuid(game_uuid) 85 cue_sheets = json.loads(self.options[Option.CUE_SHEETS]) 86 for i, cue_sheet in enumerate(cue_sheets): 87 # FIXME: Try to get this to work with the PyCharm type checker 88 path = os.path.join(temp_dir, cue_sheet["name"]) 89 if i == 0: 90 game_file = path 91 # noinspection PyTypeChecker 92 with open(path, "wb") as f: 93 # noinspection PyTypeChecker 94 f.write(cue_sheet["data"].encode("UTF-8")) 95 96 if self.options[Option.SBI_DATA]: 97 sbi_data = json.loads(self.options[Option.SBI_DATA]) 98 for i, sbi_file in enumerate(sbi_data): 99 path = os.path.join(temp_dir, sbi_file["name"]) 100 with open(path, "wb") as f: 101 f.write(b64decode(sbi_file["base64"])) 102 103 self.emulator.args.append(game_file) 104 105 def prepare_mednafen_bios(self, known_file, name): 106 bios_path = os.path.join(self.home.path, ".mednafen", name) 107 if not os.path.exists(os.path.dirname(bios_path)): 108 os.makedirs(os.path.dirname(bios_path)) 109 src = self.fsgc.file.find_by_sha1(known_file.sha1) 110 if not src: 111 raise Exception( 112 "Could not find {} (SHA-1: {}".format( 113 known_file.name, known_file.sha1 114 ) 115 ) 116 self.fsgc.file.copy_game_file(src, bios_path) 117 118 def get_supported_filters(self): 119 supported = [ 120 {"name": "2x", "special": "nn2x"}, 121 {"name": "none", "special": "none"}, 122 {"name": "scale2x", "special": "scale2x"}, 123 {"name": "hq2x", "special": "hq2x"}, 124 ] 125 return supported 126 127 def mednafen_input_mapping(self, port): 128 raise NotImplementedError() 129 130 def mednafen_system_prefix(self): 131 raise NotImplementedError() 132 133 def game_video_par(self): 134 return 1.0 135 136 def game_video_size(self): 137 raise NotImplementedError("game_video_size must be implemented") 138 139 def mednafen_rom_extensions(self): 140 return [] 141 142 def mednafen_scanlines_setting(self): 143 return None 144 145 def mednafen_special_filter(self): 146 return None 147 148 def mednafen_viewport(self): 149 viewport = self.options["viewport"] 150 if viewport: 151 src, dst = viewport.split("=") 152 src = src.strip() 153 dst = dst.strip() 154 src_x, src_y, src_w, src_h = [int(v) for v in src.split(" ")] 155 dst_x, dst_y, dst_w, dst_h = [int(v) for v in dst.split(" ")] 156 return (src_x, src_y, src_w, src_h), (dst_x, dst_y, dst_w, dst_h) 157 return None, None 158 159 def mednafen_configure(self, f): 160 pfx = self.mednafen_system_prefix() 161 self.configure_audio(f) 162 self.configure_input(f) 163 self.configure_video(f) 164 165 cheats_file_name = self.mednafen_system_prefix() + ".cht" 166 cheats_file_path = self.cheats_file(cheats_file_name) 167 if cheats_file_path: 168 self.emulator.args.extend(["-cheats", "0"]) 169 cheats_dir = self.temp_dir("cheats").path 170 shutil.copy( 171 cheats_file_path, os.path.join(cheats_dir, cheats_file_name) 172 ) 173 # self.emulator.args.extend(["-filesys.path_cheat", cheats_dir]) 174 self.emulator.args.extend(["-path_cheat", cheats_dir]) 175 176 print("\n" + "-" * 79 + "\n" + "CONFIGURE DIRS") 177 178 self.emulator.args.extend( 179 ["-path_sav", self.save_handler.emulator_save_dir()] 180 ) 181 self.emulator.args.extend( 182 ["-path_state", self.save_handler.emulator_state_dir()] 183 ) 184 185 # self.emulator.args.extend(["-filesys.fname_state", "%M%X"]) 186 # self.emulator.args.extend(["-filesys.fname_sav", "%M%x"]) 187 188 self.emulator.args.extend(["-filesys.fname_state", "%f.%X"]) 189 self.emulator.args.extend(["-filesys.fname_sav", "%f.%x"]) 190 191 # docdir = pyapp.user.documents_dir() 192 self.doc_dir = self.temp_dir("mednafen-docs") 193 self.emulator.args.extend( 194 [ 195 "-path_movie", 196 self.doc_dir.path, 197 # "-path_cheat", self.doc_dir.path, 198 "-path_palette", 199 self.home.path, 200 ] 201 ) 202 203 self.emulator.args.extend(["-path_snap", self.screenshots_dir()]) 204 self.emulator.args.extend( 205 [ 206 "-filesys.fname_snap", 207 "{0}-%p.%x".format(self.screenshots_name()), 208 ] 209 ) 210 211 self.mednafen_post_configure() 212 213 def configure_audio(self, _): 214 # pfx = self.mednafen_system_prefix() 215 audio_driver = self.options[Option.MEDNAFEN_AUDIO_DRIVER] 216 if audio_driver in ["", "auto"]: 217 if System.windows: 218 pass 219 elif System.macos: 220 pass 221 else: 222 audio_driver = "sdl" 223 if audio_driver == "sdl": 224 # Mednafen does not support PulseAudio directly, but using the 225 # sdl driver will "often" result in PulseAudio being used 226 # indirectly. 227 self.emulator.args.extend(["-sound.driver", "sdl"]) 228 elif audio_driver == "alsa": 229 self.emulator.args.extend( 230 ["-sound.device", "sexyal-literal-default"] 231 ) 232 else: 233 # Use Mednafen default selection 234 # self.emulator.args.extend(["-sound.device", "default"]) 235 pass 236 237 default_buffer_size = 40 238 buffer_size = default_buffer_size 239 if self.options[Option.MEDNAFEN_AUDIO_BUFFER]: 240 try: 241 buffer_size = int(self.options[Option.MEDNAFEN_AUDIO_BUFFER]) 242 except ValueError: 243 print("WARNING: Invalid Mednafen audio buffer size specified") 244 else: 245 if buffer_size < 0 or buffer_size > 1000: 246 print("WARNING: Mednafen audio buffer size out of range") 247 buffer_size = default_buffer_size 248 self.emulator.args.extend(["-sound.buffer_time", str(buffer_size)]) 249 250 def configure_input(self, f): 251 print("\n" + "-" * 79 + "\n" + "CONFIGURE PORTS") 252 for i, port in enumerate(self.ports): 253 input_mapping = self.mednafen_input_mapping(i) 254 mapper = MednafenInputMapper(port, input_mapping) 255 keys = {} 256 for key, value in mapper.items(): 257 keys.setdefault(key, []).append(value) 258 for key, values in keys.items(): 259 print(repr(key), repr(values)) 260 f.write( 261 "{key} {value}\n".format( 262 key=key, value=" || ".join(values) 263 ) 264 ) 265 266 def configure_video(self, f): 267 pfx = self.mednafen_system_prefix() 268 self.emulator.args.extend(["-video.driver", "opengl"]) 269 270 screen_w, screen_h = self.screen_size() 271 # screen_a = screen_w / screen_h 272 border_w, border_h = 0, 0 273 274 # if self.border() == self.NO_BORDER: 275 # pass 276 # else: 277 # border_w = screen_h * 32 / 1080 278 # border_h = screen_h * 32 / 1080 279 280 dest_w, dest_h = screen_w - border_w, screen_h - border_h 281 if not self.use_fullscreen(): 282 dest_w, dest_h = 960, 540 283 # dest_a = dest_w /dest_h 284 285 viewport_src, viewport = self.mednafen_viewport() 286 if viewport is None: 287 game_w, game_h = self.game_video_size() 288 else: 289 game_w, game_h = viewport[2], viewport[3] 290 assert game_w 291 assert game_h 292 # game_a = game_w / game_h 293 pixel_a = self.game_video_par() 294 295 if self.scaling() == self.MAX_SCALING: 296 if self.stretching() == self.STRETCH_FILL_SCREEN: 297 scale_y = dest_h / game_h 298 scale_x = dest_w / game_w 299 elif self.stretching() == self.STRETCH_ASPECT: 300 # FIXME: HANDLE TALL RESOLUTIONS 301 scale_y = dest_h / game_h 302 # scale_x = dest_w / (game_w * pixel_a) 303 scale_x = scale_y * pixel_a 304 else: # Square pixels 305 scale_y = dest_h / game_h 306 scale_x = scale_y 307 elif self.scaling() == self.INTEGER_SCALING: 308 if self.stretching() == self.STRETCH_FILL_SCREEN: 309 scale_y = dest_h // game_h 310 scale_x = dest_w // game_w 311 elif self.stretching() == self.STRETCH_ASPECT: 312 # FIXME: HANDLE TALL RESOLUTIONS 313 scale_y = dest_h // game_h 314 scale_x = scale_y * pixel_a 315 else: # Square pixels 316 scale_y = dest_h // game_h 317 scale_x = scale_y 318 else: # NO SCALING 319 scale_x = 1 320 scale_y = 1 321 322 if self.use_doubling(): 323 if scale_x == 1 and scale_y == 1: 324 scale_x = 2 325 scale_y = 2 326 327 # self.emulator.args.extend(["-{}.special".format(pfx), "nn2x"]) 328 329 if self.smoothing() == self.SMOOTHING: 330 videoip = "1" 331 elif self.smoothing() == self.NON_INTEGER_SMOOTHING: 332 if abs(scale_x - scale_x // 1) < 0.01: 333 videoip = "y" 334 else: 335 videoip = "1" 336 if abs(scale_y - scale_y // 1) < 0.01: 337 if videoip == "y": 338 videoip = "0" 339 else: 340 videoip = "x" 341 else: 342 videoip = "0" 343 self.emulator.args.extend(["-{}.videoip".format(pfx), videoip]) 344 345 self.emulator.args.extend(["-{}.xscale".format(pfx), str(scale_x)]) 346 self.emulator.args.extend(["-{}.yscale".format(pfx), str(scale_y)]) 347 self.emulator.args.extend(["-{}.xscalefs".format(pfx), str(scale_x)]) 348 self.emulator.args.extend(["-{}.yscalefs".format(pfx), str(scale_y)]) 349 self.emulator.args.extend(["-{}.stretch".format(pfx), "0"]) 350 351 # Only enable v-sync if game refresh matches screen refresh. 352 if self.configure_vsync(): 353 self.emulator.args.extend(["-video.glvsync", "1"]) 354 else: 355 self.emulator.args.extend(["-video.glvsync", "0"]) 356 357 # Specify fullscreen size and conditionally enable fullscreen mode. 358 self.emulator.args.extend(["-{}.xres".format(pfx), str(screen_w)]) 359 self.emulator.args.extend(["-{}.yres".format(pfx), str(screen_h)]) 360 if self.use_fullscreen(): 361 self.emulator.args.extend(["-fs", "1"]) 362 else: 363 self.emulator.args.extend(["-fs", "0"]) 364 365 if self.effect() == self.CRT_EFFECT: 366 self.emulator.args.extend(["-{}.shader".format(pfx), "goat"]) 367 self.emulator.args.extend( 368 ["-{}.shader.goat.slen".format(pfx), "1"] 369 ) 370 special = "none" 371 video_scale = 2 372 min_video_scale = 2 373 elif self.effect() == self.DOUBLE_EFFECT: 374 special = "nn2x" 375 video_scale = 2 376 min_video_scale = 1 377 elif self.effect() == self.HQ2X_EFFECT: 378 special = "hq2x" 379 video_scale = 2 380 min_video_scale = 2 381 elif self.effect() == self.SCALE2X_EFFECT: 382 special = "scale2x" 383 video_scale = 2 384 min_video_scale = 2 385 else: 386 special = "none" 387 video_scale = 1 388 min_video_scale = 1 389 390 if self.scaling() == self.MAX_SCALING: 391 window_w, window_h = 960, 540 392 elif self.scaling() == self.INTEGER_SCALING: 393 window_w = game_w * min_video_scale 394 window_h = game_h * min_video_scale 395 s = min_video_scale + 1 396 print(window_w, window_h) 397 while game_w * s <= 900 and game_h * s <= 700: 398 window_w = game_w * s 399 window_h = game_h * s 400 s += 1 401 else: 402 window_w = game_w * video_scale 403 window_h = game_h * video_scale 404 self.emulator.env["FSGS_WINDOW_SIZE"] = "{},{}".format( 405 window_w, window_h 406 ) 407 408 deinterlacer = self.options[Option.MEDNAFEN_DEINTERLACER] 409 if deinterlacer: 410 self.emulator.args.extend(["-video.deinterlacer", deinterlacer]) 411 412 temporal_blur = self.options[Option.MEDNAFEN_TEMPORAL_BLUR] == "1" 413 if temporal_blur: 414 self.emulator.args.extend(["-{}.tblur".format(pfx), "1"]) 415 416 self.emulator.args.extend(["-{}.special".format(pfx), special]) 417 self.emulator.args.extend(self.mednafen_extra_graphics_options()) 418 419 def set_mednafen_input_order(self): 420 if System.windows: 421 self.input_device_order = "DINPUT8" 422 self.input_mapping_multiple = False 423 424 def mednafen_extra_graphics_options(self): 425 return [] 426 427 def mednafen_post_configure(self): 428 # can be overridden by subclasses 429 pass 430 431 def mednafen_cfg_path(self): 432 if not os.path.exists(os.path.join(self.home.path, ".mednafen")): 433 os.makedirs(os.path.join(self.home.path, ".mednafen")) 434 # return os.path.join(self.home.path, ".mednafen", "mednafen-09x.cfg") 435 return os.path.join(self.home.path, ".mednafen", "mednafen.cfg") 436 437 def is_pal(self): 438 # return self.config.get("ntsc_mode") != "1" 439 # return False 440 refresh_rate = self.get_game_refresh_rate() 441 if refresh_rate: 442 return int(round(refresh_rate)) == 50 443 444 445class MednafenInputMapper(InputMapper): 446 def __init__(self, port, mapping): 447 InputMapper.__init__(self, port, mapping) 448 helper = EnumerateHelper() 449 helper.init() 450 seen_ids = set() 451 self.id_map = {} 452 for device in helper.devices: 453 uid = self.calculate_unique_id(device) 454 while uid in seen_ids: 455 uid += 1 456 seen_ids.add(uid) 457 self.id_map[device.id] = uid 458 print("MednafenInputMapper device map") 459 for id, uid in self.id_map.items(): 460 print(uid, id) 461 462 def axis(self, axis, positive): 463 if positive: 464 sign = "+" 465 else: 466 sign = "-" 467 joystick_id = self.unique_id(self.device, self.device.id) 468 return "joystick 0x{:032x} abs_{}{}".format(joystick_id, axis, sign) 469 470 def hat(self, hat, direction): 471 offset = {"left": 3, "right": 1, "up": 0, "down": 2}[direction] 472 joystick_id = self.unique_id(self.device, self.device.id) 473 # Hats after buttons, order: up, right, down, left 474 return "joystick 0x{:032x} button_{}".format( 475 joystick_id, self.device.buttons + hat * 4 + offset 476 ) 477 478 def button(self, button): 479 joystick_id = self.unique_id(self.device, self.device.id) 480 return "joystick 0x{:032x} button_{}".format(joystick_id, button) 481 482 def key(self, key): 483 # FIXME: Need other key codes on Windows ... ? 484 # print(key) 485 return "keyboard 0x0 {}".format(key.sdl2_scan_code) 486 487 @lru_cache() 488 def unique_id(self, device, _): 489 try: 490 return self.id_map[device.id] 491 except KeyError: 492 print("id_map:", self.id_map) 493 raise 494 495 @lru_cache() 496 def calculate_unique_id(self, device, version=2): 497 """Implements the joystick ID algorithm in mednafen. 498 Was src/drivers/joystick.cpp:GetJoystickUniqueID 499 Now src/drivers/Joystick.cpp:Calc09xID. 500 """ 501 print("get_unique_id for", device.id) 502 if version == 2: 503 print(device) 504 m = hashlib.md5() 505 print("--------------{}----------------".format(device.sdl_name)) 506 m.update((device.sdl_name).encode("UTF-8")) 507 # print(m.hexdigest()[:16], device.axes, device.buttons, device.hats, device.balls) 508 509 buffer = struct.pack( 510 ">HHHH", device.axes, device.buttons, device.hats, device.balls 511 ) 512 return int( 513 "{}{}".format( 514 hexlify(m.digest()[:8]).decode("ASCII"), 515 hexlify(buffer).decode("ASCII"), 516 ), 517 16, 518 ) 519 # return "0x{}{:02x}{:02x}{:02x}{:02x}".format( 520 # m.hexdigest()[:16], device.axes, device.buttons, device.hats, device.balls) 521 else: 522 m = hashlib.md5() 523 print(device.axes, device.balls, device.hats, device.buttons) 524 # noinspection SpellCheckingInspection 525 buffer = struct.pack( 526 "iiii", device.axes, device.balls, device.hats, device.buttons 527 ) 528 m.update(buffer) 529 digest = m.digest() 530 ret = 0 531 for x in range(16): 532 # ret ^= ord(digest[x]) << ((x & 7) * 8) 533 ret ^= digest[x] << ((x & 7) * 8) 534 # return "{:x}".format(ret) 535 return ret 536 537 538class MednafenSaveHandler(SaveHandler): 539 def __init__(self, fsgc, options): 540 super().__init__(fsgc, options, emulator="Mednafen") 541 self._srm_alias = "" 542 543 def prepare(self): 544 self.copy_to_srm_alias() 545 super().prepare() 546 547 def finish(self): 548 self.move_from_srm_alias() 549 super().finish() 550 551 def set_srm_alias(self, alias): 552 assert alias.startswith(".") 553 self._srm_alias = alias 554 555 def copy_to_srm_alias(self): 556 if not self._srm_alias: 557 return 558 save_dir = self.save_dir() 559 if not os.path.exists(save_dir): 560 return 561 for full_name in os.listdir(save_dir): 562 src = os.path.join(save_dir, full_name) 563 if not os.path.isfile(src): 564 continue 565 name, ext = os.path.splitext(full_name) 566 if ext not in [".srm"]: 567 continue 568 dst = os.path.join(save_dir, name + self._srm_alias) 569 print("MednafenSaveHandler:", src, "->", dst) 570 shutil.copy(src, dst) 571 572 def move_from_srm_alias(self): 573 if not self._srm_alias: 574 return 575 save_dir = self.save_dir() 576 for full_name in os.listdir(save_dir): 577 src = os.path.join(save_dir, full_name) 578 if not os.path.isfile(src): 579 continue 580 name, ext = os.path.splitext(full_name) 581 if ext != self._srm_alias: 582 continue 583 dst = os.path.join(save_dir, name + ".srm") 584 print("MednafenSaveHandler:", dst, "<-", src) 585 shutil.copy(src, dst) 586 os.remove(src) 587