Repository: lutris/lutris

Issue:
EasyRPG runner
A standalone EasyRPG runner (aside from the one available in Libretro) would be nice to have.
I can try my hand at submitting a pull request for it once I have some time to spare.


Discussion:

Resolving patch:
diff --git a/lutris/runners/__init__.py b/lutris/runners/__init__.py
--- a/lutris/runners/__init__.py
+++ b/lutris/runners/__init__.py
@@ -10,6 +10,7 @@
     "winesteam",
     "dosbox",
     # Multi-system
+    "easyrpg",
     "mame",
     "mednafen",
     "scummvm",
diff --git a/lutris/runners/easyrpg.py b/lutris/runners/easyrpg.py
new file mode 100644
--- /dev/null
+++ b/lutris/runners/easyrpg.py
@@ -0,0 +1,411 @@
+# Standard Library
+from gettext import gettext as _
+from os import path
+
+# Lutris Modules
+from lutris.runners.runner import Runner
+
+
+class easyrpg(Runner):
+    human_name = _("EasyRPG Player")
+    description = _("Runs RPG Maker 2000/2003 games")
+    platforms = [_("Linux")]
+    runnable_alone = True
+    entry_point_option = "project_path"
+    runner_executable = "easyrpg-player"
+    download_url = "https://easyrpg.org/downloads/player/0.6.2.3/easyrpg-player-0.6.2.3-linux.tar.gz"
+
+    game_options = [
+        {
+            "option": "project_path",
+            "type": "directory_chooser",
+            "label": _("Game directory"),
+            "help": _("Select the directory of the game. (required)")
+        },
+        {
+            "option": "encoding",
+            "type": "string",
+            "label": _("Encoding"),
+            "help": _(
+                "Instead of auto detecting the encoding or using the "
+                "one in RPG_RT.ini, the specified encoding is used. "
+                "Use 'auto' for automatic detection."
+            )
+        },
+        {
+            "option": "engine",
+            "type": "choice",
+            "label": _("Engine"),
+            "help": _("Disable auto detection of the simulated engine."),
+            "choices": [
+                (_("Auto"), ""),
+                (_("RPG Maker 2000 engine (v1.00 - v1.10)"), "rpg2k"),
+                (_("RPG Maker 2000 engine (v1.50 - v1.51)"), "rpg2kv150"),
+                (_("RPG Maker 2000 (English release) engine"), "rpg2ke"),
+                (_("RPG Maker 2003 engine (v1.00 - v1.04)"), "rpg2k3"),
+                (_("RPG Maker 2003 engine (v1.05 - v1.09a)"), "rpg2k3v105"),
+                (_("RPG Maker 2003 (English release) engine"), "rpg2k3e")
+            ],
+            "default": ""
+        },
+        {
+            "option": "save_path",
+            "type": "directory_chooser",
+            "label": _("Save path"),
+            "help": _(
+                "Instead of storing save files in the game directory they "
+                "are stored in the specified path. The directory must exist."
+            )
+        },
+        {
+            "option": "new_game",
+            "type": "bool",
+            "label": _("New game"),
+            "help": _("Skip the title scene and start a new game directly."),
+            "default": False
+        },
+        {
+            "option": "load_game_id",
+            "type": "range",
+            "label": _("Load game ID"),
+            "help": _(
+                "Skip the title scene and load SaveXX.lsd. "
+                "Set to '0' to disable."
+            ),
+            "min": 0,
+            "max": 99,
+            "default": 0
+        },
+        {
+            "option": "start_map_id",
+            "type": "range",
+            "label": _("Start map ID"),
+            "help": _(
+                "Overwrite the map used for new games and use "
+                "MapXXXX.lmu instead. Set to '0' to disable. "
+                "\n\nIncompatible with 'Load game ID'."
+            ),
+            "min": 0,
+            "max": 9999,
+            "default": 0
+        },
+        {
+            "option": "start_position",
+            "type": "string",
+            "label": _("Start position"),
+            "help": _(
+                "Overwrite the party start position and "
+                "move the party to the specified position. "
+                "Provide two numbers separated by a space. "
+                "\n\nIncompatible with 'Load game ID'."
+            )
+        },
+        {
+            "option": "start_party",
+            "type": "string",
+            "label": _("Start party"),
+            "help": _(
+                "Overwrite the starting party members with "
+                "the actors with the specified IDs. Provide "
+                "one to four numbers separated by spaces. "
+                "\n\nIncompatible with 'Load game ID'."
+            )
+        },
+        {
+            "option": "battle_test",
+            "type": "string",
+            "label": _("Monster party"),
+            "help": _("Start a battle test with the specified monster party.")
+        },
+        {
+            "option": "record_input",
+            "type": "string",
+            "label": _("Record input"),
+            "help": _("Records all button input to the specified log file.")
+        },
+        {
+            "option": "replay_input",
+            "type": "file",
+            "label": _("Replay input"),
+            "help": _(
+                "Replays button input from the specified log file, "
+                "as generated by 'Record input'. If the RNG seed "
+                "and the state of the save file directory is also "
+                "the same as it was when the log was recorded, this "
+                "should reproduce an identical run to the one recorded."
+            )
+        },
+    ]
+
+    runner_options = [
+        {
+            "option": "fullscreen",
+            "type": "bool",
+            "label": _("Fullscreen"),
+            "help": _("Start in fullscreen mode."),
+            "default": False
+        },
+        {
+            "option": "audio",
+            "type": "bool",
+            "label": _("Enable audio"),
+            "help": _(
+                "Switch off to disable audio "
+                "(in case you prefer your own music)."
+            ),
+            "default": True
+        },
+        {
+            "option": "mouse",
+            "type": "bool",
+            "label": _("Enable mouse"),
+            "help": _(
+                "Use mouse click for decision and scroll wheel for lists."
+            ),
+            "default": False
+        },
+        {
+            "option": "touch",
+            "type": "bool",
+            "label": _("Enable touch"),
+            "help": _("Use one/two finger tap for decision/cancel."),
+            "default": False
+        },
+        {
+            "option": "hide_title",
+            "type": "bool",
+            "label": _("Hide title"),
+            "help": _(
+                "Hide the title background image and center the command menu."
+            ),
+            "default": False
+        },
+        {
+            "option": "vsync",
+            "type": "bool",
+            "label": _("Enable VSync"),
+            "help": _(
+                "Switch off to disable VSync and use the FPS limit. "
+                "VSync may or may not be supported on all platforms."
+            ),
+            "default": True
+        },
+        {
+            "option": "fps_limit",
+            "type": "string",
+            "label": _("FPS limit"),
+            "help": _(
+                "Set a custom frames per second limit. If unspecified, "
+                "the default is 60 FPS. Set to '0' to disable the frame "
+                "limiter. This option may not be supported on all platforms."
+            )
+        },
+        {
+            "option": "show_fps",
+            "type": "choice",
+            "label": _("Show FPS"),
+            "help": _("Enable frames per second counter."),
+            "choices": [
+                (_("Disabled"), "off"),
+                (_("Fullscreen & title bar"), "on"),
+                (_("Fullscreen, title bar & window"), "full")
+            ],
+            "default": "off"
+        },
+        {
+            "option": "seed",
+            "type": "string",
+            "label": _("RNG seed"),
+            "help": _("Seeds the random number generator")
+        },
+        {
+            "option": "test_play",
+            "type": "bool",
+            "label": _("Test play"),
+            "help": _("Enable TestPlay mode."),
+            "default": False
+        },
+        {
+            "option": "rtp",
+            "type": "bool",
+            "label": _("Enable RTP"),
+            "help": _(
+                "Switch off to disable support for the Runtime Package (RTP)."
+            ),
+            "default": True
+        },
+        {
+            "option": "rpg2k_rtp_path",
+            "type": "directory_chooser",
+            "label": _("RPG2000 RTP location"),
+            "help": _(
+                "Full path to a directory containing an "
+                "extracted RPG Maker 2000 Run-Time-Package (RTP)."
+            )
+        },
+        {
+            "option": "rpg2k3_rtp_path",
+            "type": "directory_chooser",
+            "label": _("RPG2003 RTP location"),
+            "help": _(
+                "Full path to a directory containing an "
+                "extracted RPG Maker 2003 Run-Time-Package (RTP)."
+            )
+        },
+        {
+            "option": "rpg_rtp_path",
+            "type": "directory_chooser",
+            "label": _("Fallback RTP location"),
+            "help": _("Full path to a directory containing a combined RTP.")
+        },
+    ]
+
+    @property
+    def game_path(self):
+        game_path = self.game_data.get("directory")
+        if game_path:
+            return game_path
+
+        # Default to the directory of the entry point
+        entry_point = self.game_config.get(self.entry_point_option)
+        if entry_point:
+            return path.expanduser(entry_point)
+
+        return ""
+
+    def get_env(self, os_env=False):
+        env = super().get_env(os_env)
+
+        rpg2k_rtp_path = self.runner_config.get("rpg2k_rtp_path")
+        if rpg2k_rtp_path:
+            env["RPG2K_RTP_PATH"] = rpg2k_rtp_path
+
+        rpg2k3_rtp_path = self.runner_config.get("rpg2k3_rtp_path")
+        if rpg2k3_rtp_path:
+            env["RPG2K3_RTP_PATH"] = rpg2k3_rtp_path
+
+        rpg_rtp_path = self.runner_config.get("rpg_rtp_path")
+        if rpg_rtp_path:
+            env["RPG_RTP_PATH"] = rpg_rtp_path
+
+        return env
+
+    def get_runner_command(self):
+        cmd = [self.get_executable()]
+
+        if self.runner_config["fullscreen"]:
+            cmd.append("--fullscreen")
+        else:
+            cmd.append("--window")
+
+        if not self.runner_config["audio"]:
+            cmd.append("--disable-audio")
+
+        if self.runner_config["mouse"]:
+            cmd.append("--enable-mouse")
+
+        if self.runner_config["touch"]:
+            cmd.append("--enable-touch")
+
+        if self.runner_config["hide_title"]:
+            cmd.append("--hide-title")
+
+        if not self.runner_config["vsync"]:
+            cmd.append("--no-vsync")
+
+        fps_limit = self.runner_config.get("fps_limit")
+        if fps_limit:
+            cmd.extend(("--fps-limit", fps_limit))
+
+        show_fps = self.runner_config.get("show_fps")
+        if show_fps != "off":
+            cmd.append("--show-fps")
+        if show_fps == "full":
+            cmd.append("--fps-render-window")
+
+        if self.runner_config["test_play"]:
+            cmd.append("--test-play")
+
+        seed = self.runner_config.get("seed")
+        if seed:
+            cmd.extend(("--seed", seed))
+
+        if not self.runner_config["rtp"]:
+            cmd.append("--disable-rtp")
+
+        return cmd
+
+    def get_run_data(self):
+        cmd = self.get_runner_command()
+
+        if self.default_path:
+            game_path = path.expanduser(self.default_path)
+            cmd.extend(("--project-path", game_path))
+
+        return {"command": cmd, "env": self.get_env()}
+
+    def play(self):
+        if not self.game_path:
+            return {"error": "CUSTOM", "text": _("No game directory provided")}
+        if not path.isdir(self.game_path):
+            return self.directory_not_found(self.game_path)
+
+        cmd = self.get_runner_command()
+
+        cmd.extend(("--project-path", self.game_path))
+
+        encoding = self.game_config.get("encoding")
+        if encoding:
+            cmd.extend(("--encoding", encoding))
+
+        engine = self.game_config.get("engine")
+        if engine:
+            cmd.extend(("--engine", engine))
+
+        save_path = self.game_config.get("save_path")
+        if save_path:
+            save_path = path.expanduser(save_path)
+            if not path.isdir(save_path):
+                return self.directory_not_found(save_path)
+            cmd.extend(("--save-path", save_path))
+
+        record_input = self.game_config.get("record_input")
+        if record_input:
+            record_input = path.expanduser(record_input)
+            cmd.extend(("--record-input", record_input))
+
+        replay_input = self.game_config.get("replay_input")
+        if replay_input:
+            replay_input = path.expanduser(replay_input)
+            if not path.isfile(replay_input):
+                return {"error": "FILE_NOT_FOUND", "file": replay_input}
+            cmd.extend(("--replay-input", replay_input))
+
+        load_game_id = self.game_config.get("load_game_id")
+        if load_game_id:
+            cmd.extend(("--load-game-id", str(load_game_id)))
+
+        start_map_id = self.game_config.get("start_map_id")
+        if start_map_id:
+            cmd.extend(("--start-map-id", str(start_map_id)))
+
+        start_position = self.game_config.get("start_position")
+        if start_position:
+            cmd.extend(("--start-position", *start_position.split()))
+
+        start_party = self.game_config.get("start_party")
+        if start_party:
+            cmd.extend(("--start-party", *start_party.split()))
+
+        battle_test = self.game_config.get("battle_test")
+        if battle_test:
+            cmd.extend(("--battle-test", battle_test))
+
+        return {"command": cmd}
+
+    @staticmethod
+    def directory_not_found(directory):
+        error = _(
+            "The directory {} could not be found"
+        ).format(directory.replace("&", "&amp;"))
+        return {"error": "CUSTOM", "text": error}


Test patch: