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