1import os
2import shutil
3from typing import Optional
4
5from fsgs import Option
6from fsgs.FSGSDirectories import FSGSDirectories
7from fsgs.drivers.gamedriver import GameDriver, Emulator
8from fsgs.input.mapper import InputMapper
9from fsgs.plugins.pluginmanager import PluginManager
10from fsgs.saves import SaveHandler
11
12
13class RetroArchDriver(GameDriver):
14    def __init__(self, fsgc, libretro_core, retroarch_state_dir):
15        super().__init__(fsgc)
16        self.emulator = Emulator("retroarch-fs")
17        # self.emulator.allow_system_emulator = True
18        # self.libretro_core = "no_core_specified"
19        self.system_dir = self.temp_dir("system")
20        self.save_handler = RetroArchSaveHandler(
21            self.fsgc, options=self.options, emulator=retroarch_state_dir
22        )
23        # self.retroarch_state_dir = None
24
25        self.libretro_core = libretro_core
26        self.retroarch_state_dir = retroarch_state_dir
27        # Dictionary with key-values which will be written to retroarch.cfg
28        self.retroarch_config_file = self.temp_file("retroarch.cfg")
29        self.retroarch_config = {}
30        # FIXME
31        self.retroarch_core_config = {}
32
33    def prepare(self):
34        self.erase_old_config()
35
36        # if not os.path.exists(config_dir):
37        #     os.makedirs(config_dir)
38        # config_file = os.path.join(config_dir, "retroarch.cfg")
39
40        # FIXME: Do not use /etc/retroarch.cfg as template.. how to prevent?
41
42        self.save_handler.prepare()
43
44        with open(self.retroarch_config_file.path, "w", encoding="UTF-8") as f:
45            self.write_retroarch_config(f)
46            self.write_retroarch_input_config(f)
47            self.write_retroarch_video_config(f)
48
49        self.emulator.args.append(
50            "--appendconfig=" + self.retroarch_config_file.path
51        )
52        if self.use_fullscreen():
53            self.emulator.args.append("--fullscreen")
54
55        libretro_core = self.find_libretro_core(self.libretro_core)
56        if not libretro_core:
57            raise Exception(
58                "Could not find libretro core {0!r}".format(self.libretro_core)
59            )
60        self.emulator.args.extend(["-L", libretro_core])
61
62        # Verbose logging
63        self.emulator.args.extend(["-v"])
64
65    def run(self):
66        with open(self.retroarch_config_file.path, "a", encoding="UTF-8") as f:
67            for key, value in self.retroarch_config.items():
68                f.write('{} = "{}"\n'.format(key, value))
69        super().run()
70
71    def finish(self):
72        self.save_handler.finish()
73
74    @staticmethod
75    def find_libretro_core(name):
76        # return "/usr/lib/x86_64-linux-gnu/libretro/{}.so".format(name)
77        return PluginManager.instance().find_library_path(name)
78
79    @staticmethod
80    def find_retroarch_shader(name):
81        # FIXME: Better to find data file based on path/provides rather than
82        # hardcoding plugin name, but...
83        plugin = PluginManager.instance().plugin("RetroArch-FS")
84        return plugin.data_file_path("shaders/shaders_glsl/" + name + ".glslp")
85
86    def display_aspect_ratio(self) -> Optional[float]:
87        return 4 / 3
88
89    def game_video_size(self):
90        # FIXME: Dummy values
91        print("[DRIVER] Warning: Using dummy game video size (320, 240)")
92        return 320, 240
93
94    def erase_old_config(self):
95        config_dir = os.path.join(self.home.path, ".config", "retroarch")
96        if os.path.exists(config_dir):
97            shutil.rmtree(config_dir)
98
99    def open_retroarch_core_options(self):
100        config_dir = os.path.join(self.home.path, ".config", "retroarch")
101        if not os.path.exists(config_dir):
102            os.makedirs(config_dir)
103        config_file = os.path.join(config_dir, "retroarch-core-options.cfg")
104        return open(config_file, "w", encoding="UTF-8")
105
106    def write_retroarch_config(self, f):
107        """
108        joypad_autoconfig_dir = "~/.config/retroarch/autoconfig"
109        """
110        f.write(
111            'screenshot_directory = "{}"\n'.format(
112                FSGSDirectories.screenshots_output_dir()
113            )
114        )
115        f.write('system_directory = "{}"\n'.format(self.system_dir.path))
116        # noinspection SpellCheckingInspection
117
118        f.write(
119            'savefile_directory = "{}"\n'.format(
120                self.save_handler.emulator_save_dir()
121            )
122        )
123        f.write(
124            'savestate_directory = "{}"\n'.format(
125                self.save_handler.emulator_state_dir()
126            )
127        )
128
129        # FIXME: Maybe enable autosave to save .srm while running the emulator
130        # and not only on shutdown?
131        # f.write("autosave_interval = 60\n")
132
133        f.write("pause_nonactive = false\n")
134        f.write("video_font_enable = false\n")
135        f.write("rgui_show_start_screen = false\n")
136        f.write("all_users_control_menu = true\n")
137        f.write("video_gpu_screenshot = false\n")
138
139        if self.g_sync():
140            # Without timed frame limiting, there will be stuttering (probably)
141            # due to (some) audio driver sync not being stable enough to give
142            # a stable frame rate.
143
144            # FIXME: Implement better G-SYNC method in RetroArch
145            f.write("fastforward_ratio = 1.000000\n")
146
147            # FIXME: It's possible the above "fix" would be better for
148            # non-v-sync as well, if the audio sync is not stable.
149
150            # f.write("audio_max_timing_skew = 0.0\n")
151            # f.write("audio_rate_control_delta = 0.0\n")
152            # f.write("video_refresh_rate = 144.0\n")
153            # f.write("audio_sync = false\n")
154
155        default_buffer_size = 40
156        buffer_size = default_buffer_size
157        if self.options[Option.RETROARCH_AUDIO_BUFFER]:
158            try:
159                buffer_size = int(self.options[Option.RETROARCH_AUDIO_BUFFER])
160            except ValueError:
161                print("WARNING: Invalid RetroArch audio buffer size specified")
162            else:
163                if buffer_size < 0 or buffer_size > 1000:
164                    print("WARNING: RetroArch audio buffer size out of range")
165                    buffer_size = default_buffer_size
166        f.write("audio_latency = {}\n".format(buffer_size))
167
168    def write_retroarch_input_config(self, f):
169        f.write('input_driver = "sdl2"\n')
170        f.write('input_enable_hotkey = "alt"\n')
171        f.write('input_exit_emulator = "q"\n')
172        f.write('input_toggle_fast_forward = "w"\n')
173        f.write('input_screenshot = "s"\n')
174        # f.write("input_toggle_fullscreen = \"enter\"\n")
175        f.write('input_toggle_fullscreen = "f"\n')
176        f.write('input_audio_mute = "m"\n')
177        f.write('input_menu_toggle = "f12"\n')
178        f.write('input_pause_toggle = "p"\n')
179
180        for i, port in enumerate(self.ports):
181            if port.device is None:
182                continue
183            input_mapping = self.retroarch_input_mapping(i)
184            # FIXME: EXCLUDE DUPLICATE ITEMS IN INPUT MAPPING???
185            mapper = RetroArchInputMapper(port, input_mapping)
186
187            f.write('input_player1_joypad_index = "0"\n')
188            f.write('input_player1_analog_dpad_mode = "0"\n')
189            if port.device.type == "joystick":
190                pass
191            else:
192                pass
193
194            for key, value in mapper.items():
195                # print("---->", key, value)
196                postfix, value = value
197                f.write('{}{} = "{}"\n'.format(key, postfix, value))
198                postfixes = ["", "_btn", "_axis"]
199                postfixes.remove(postfix)
200                for postfix in postfixes:
201                    f.write('{}{} = "{}"\n'.format(key, postfix, "nul"))
202
203    def write_retroarch_video_config(self, f):
204        # f.write("video_driver = \"gl\"\n")
205        # FIXME
206        if self.fullscreen_window_mode():
207            f.write("video_windowed_fullscreen = true\n")
208        else:
209            f.write("video_windowed_fullscreen = false\n")
210
211        # FIXME: 1 or 0?
212        f.write("video_max_swapchain_images = 1\n")
213
214        if self.effect() == self.CRT_EFFECT:
215            video_shader = "crt/crt-aperture"
216            video_scale = 2
217        elif self.effect() == self.DOUBLE_EFFECT:
218            if self.smoothing() == self.NO_SMOOTHING:
219                video_shader = ""
220            else:
221                video_shader = "retro/sharp-bilinear-2x-prescale"
222            video_scale = 2
223        elif self.effect() == self.HQ2X_EFFECT:
224            video_shader = "hqx/hq2x"
225            video_scale = 2
226        elif self.effect() == self.SCALE2X_EFFECT:
227            video_shader = "scalenx/scale2x"
228            video_scale = 2
229        else:
230            video_shader = ""
231            video_scale = 1
232
233        if self.smoothing() == self.NO_SMOOTHING or video_shader:
234            f.write("video_smooth = false\n")
235
236        if video_shader:
237            print("[DRIVER] Video shader:", video_shader)
238            video_shader_path = self.find_retroarch_shader(video_shader)
239            print("[DRIVER] Video shader path:", video_shader_path)
240            if video_shader_path:
241                f.write('video_shader = "{}"\n'.format(video_shader_path))
242                f.write("video_shader_enable = true\n")
243
244        # FIXME: video_monitor_index = 0
245        # FIXME: video_disable_composition = true
246        if self.use_vsync():
247            f.write("video_vsync = true\n")
248        else:
249            f.write("video_vsync = false\n")
250
251        # aspect_ratio_index = 22
252        # custom_viewport_width = 0
253        # custom_viewport_height = 0
254        # custom_viewport_x = 0
255        # custom_viewport_y = 0
256
257        if self.stretching() == self.STRETCH_ASPECT:
258            aspect_ratio = self.display_aspect_ratio()
259            if aspect_ratio is not None:
260                f.write("aspect_ratio_index = 19\n")
261                f.write("video_aspect_ratio = {:f}\n".format(aspect_ratio))
262                # f.write("video_force_aspect = \"true\"\n")
263                # f.write("video_aspect_ratio_auto = \"false\"\n")
264        elif self.stretching() == self.STRETCH_FILL_SCREEN:
265            screen_w, screen_h = self.screen_size()
266            display_aspect = screen_w / screen_h
267            f.write("aspect_ratio_index = 19\n")
268            f.write("video_aspect_ratio = {:f}\n".format(display_aspect))
269        else:
270            f.write("aspect_ratio_index = 20\n")
271
272        if self.scaling() == self.NO_SCALING:
273            # FIXME: Window size rounding issues? E.g. 897x672 for 4:3
274            # NES 3x display. Maybe set window size manually instead
275            f.write("video_scale = {}\n".format(video_scale))
276
277        # f.write("input_osk_toggle = \"nul\"\n")
278        overlay_path = self.create_retroarch_layout()
279        if overlay_path:
280            f.write('input_overlay = "{}"\n'.format(overlay_path))
281            f.write('input_overlay_opacity = "1.000000"\n')
282
283    def retroarch_input_mapping(self, port):
284        return {}
285
286    def display_rect_fullscreen(self):
287        # FIXME: Check square pixels option!
288
289        screen_w, screen_h = self.screen_size()
290        screen_aspect = screen_w / screen_h
291
292        if self.stretching() == self.STRETCH_ASPECT:
293            display_aspect = self.display_aspect_ratio()
294        elif self.stretching() == self.STRETCH_FILL_SCREEN:
295            screen_w, screen_h = self.screen_size()
296            display_aspect = screen_w / screen_h
297        else:
298            game_w, game_h = self.game_video_size()
299            display_aspect = game_w / game_h
300
301        # FIXME: round to nearest multiple of two?
302        if screen_aspect >= display_aspect:
303            game_h = screen_h
304            game_w = round(game_h * display_aspect)
305        else:
306            game_w = screen_w
307            game_h = round(game_w / display_aspect)
308        game_x = round((screen_w - game_w) / 2)
309        game_y = round((screen_h - game_h) / 2)
310        return game_x, game_y, game_w, game_h
311
312    def create_retroarch_layout(self):
313        if self.stretching() == self.STRETCH_FILL_SCREEN or not self.bezel():
314            return
315        if not self.use_fullscreen():
316            return
317
318        # FIXME: file cmp?
319        paths = self.prepare_emulator_skin()
320        print(paths)
321
322        # FIXME: SUPPORT frame = 0 ( bezel = 0) option
323
324        # FIXME: With no bezel, we should still use a black bezel to
325        # hide screen stretching
326
327        screen_width, screen_height = self.screen_size()
328        # dst_x = 0
329        dst_y = 0
330        # dst_w = 0
331        # dst_w = 160
332        dst_h = screen_height
333
334        # Bezel size is normalized against 1080 (height)
335        scale = screen_height / 1080
336        # Bezel width: 160
337        dst_w = round(160 * scale)
338
339        game_x, game_y, game_w, game_h = self.display_rect_fullscreen()
340
341        from fsui.qt import Qt, QImage, QPainter, QRect, QSize
342
343        image = QImage(
344            QSize(screen_width, screen_height), QImage.Format_RGBA8888
345        )
346        image.fill(Qt.transparent)
347        # painter = image.paintEngine()
348        painter = QPainter(image)
349
350        dst_x = game_x - dst_w
351        left = QImage(paths["left"])
352        painter.drawImage(QRect(dst_x, dst_y, dst_w, dst_h), left)
353
354        dst_x = game_x + game_w
355        right = QImage(paths["right"])
356        painter.drawImage(QRect(dst_x, dst_y, dst_w, dst_h), right)
357        painter.end()
358
359        overlay_png_file = self.temp_file("overlay.png").path
360        image.save(overlay_png_file)
361
362        # noinspection SpellCheckingInspection
363        overlay_config = """overlays = 1
364overlay0_overlay = {overlay}
365overlay0_full_screen = true
366overlay0_rect = "0.0,0.0,1.0,1.0"
367overlay0_descs = 0
368""".format(
369            overlay=overlay_png_file
370        )
371        #         overlay_config = (
372        #             """overlays = 2
373        # overlay0_overlay = {left}
374        # overlay0_full_screen = true
375        # overlay0_rect = "0.0,0.0,0.12,1.0"
376        # overlay0_descs = 0
377        # overlay1_overlay = {right}
378        # overlay1_full_screen = true
379        # overlay1_rect = "0.8,0.0,0.2,1.0"
380        # overlay1_descs = 0
381        #
382        # """.format(left=paths["left"], right=paths["right"]))
383        overlay_config_file = self.temp_file("overlay.cfg")
384        with open(overlay_config_file.path, "w") as f:
385            f.write(overlay_config)
386        return overlay_config_file.path
387
388    def window_size(self):
389        return 0, 0
390
391
392class RetroArchInputMapper(InputMapper):
393    def __init__(self, port, mapping):
394        super().__init__(port, mapping, multiple=False)
395
396    def axis(self, axis, positive):
397        dir_str = "+" if positive else "-"
398        return "_axis", dir_str + str(axis)
399
400    def hat(self, hat, direction):
401        return "_btn", "h{}{}".format(hat, direction)
402
403    def button(self, button):
404        return "_btn", button
405
406    def key(self, key):
407        # FIXME: Correct RetroArch key names
408        name = key.sdl_name[5:].lower()
409        if name == "return":
410            # FIXME: HACK
411            name = "enter"
412        return "", name
413
414
415class RetroArchSaveHandler(SaveHandler):
416    def __init__(self, fsgc, options, emulator):
417        super().__init__(fsgc, options, emulator=emulator)
418
419    def prepare(self):
420        super().prepare()
421
422    def finish(self):
423        super().finish()
424