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