From 64e82d933afbaba23281659e275403f9fdc5a559 Mon Sep 17 00:00:00 2001 From: Alexandre Derumier Date: Mon, 27 Jul 2026 22:45:48 +0200 Subject: [PATCH] ikemen: rewrite the generator, with wine and writable squashfs support Ikemen engine code is pretty unstable, and each game can run different engine version. Some games are provided both ikemen linux bin && windows .exe, some other notes. This patch try to run game in order: 1) check if autorun.cmd is present -> launch game with wine 2) check if Ikemen_GO_Linux if present in game directory -> launch game with provided linux bin 3) Launch game with batocera system ikemen The .pc extension with wine was never implemented in generator and was not working. This patch add also support for .squashfs, and config.ini for newer ikemen versions This patch depend of https://github.com/batocera-linux/batocera.linux/pull/16094 for wine code --- .../generators/ikemen/ikemenGenerator.py | 204 ++++++++++++++++-- .../configs/configgen-defaults.yml | 2 - .../core/batocera-scripts/batocera-scripts.mk | 1 - .../batocera-scripts/scripts/batocera-ikemen | 3 - .../batocera-es-system/es_systems.yml | 2 +- .../emulators/ikemen/ikemen.emulator.yml | 8 + 6 files changed, 195 insertions(+), 25 deletions(-) delete mode 100755 package/batocera/core/batocera-scripts/scripts/batocera-ikemen diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/generators/ikemen/ikemenGenerator.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/ikemen/ikemenGenerator.py index 123df0091db..26670d210fe 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/generators/ikemen/ikemenGenerator.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/ikemen/ikemenGenerator.py @@ -1,15 +1,62 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING +import logging +import os +import stat +from contextlib import nullcontext +from pathlib import Path +from typing import TYPE_CHECKING, Final from ... import Command from ...batoceraPaths import ensure_parents_and_open from ...controller import generate_sdl_game_controller_config +from ...utils import wine from ..Generator import Generator if TYPE_CHECKING: - from ...types import HotkeysContext + from contextlib import AbstractContextManager + + from ...types import HotkeysContext, Resolution + +_logger = logging.getLogger(__name__) + +def get_windows_exe(rom: Path, /) -> Path | None: + if not (rom / "autorun.cmd").is_file(): + return None + + return wine.get_game_exe(rom) + +def get_wine_runner() -> wine.Runner: + return wine.Runner('wine-proton', "ikemen") + +_IKEMEN_SYSTEM_BINARY: Final = Path("/usr/bin/ikemen") + +_IKEMEN_GAME_BINARY: Final = "ikemen_go_linux" + +def get_linux_binary(game_dir: Path, /) -> Path: + + binary = next((entry for entry in game_dir.glob("*") if entry.name.lower() == _IKEMEN_GAME_BINARY), None) + + if binary is None or not binary.is_file(): + return _IKEMEN_SYSTEM_BINARY + + if not os.access(binary, os.X_OK): + try: + binary.chmod(binary.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + except OSError as e: + _logger.warning("%s can't be made executable (%s), running our own binary instead", binary, e) + return _IKEMEN_SYSTEM_BINARY + + if not os.access(binary, os.X_OK): + _logger.warning("%s stayed non executable, running our own binary instead", binary) + return _IKEMEN_SYSTEM_BINARY + + _logger.info("made %s executable", binary) + + _logger.debug("linux binary: %s", binary) + + return binary Keymapping =[ { @@ -169,33 +216,154 @@ } ] +# the json Buttons arrays above are positional, the engine reads them into +# KeyConfig{Joy, dU, dD, dL, dR, kA, kB, kC, kX, kY, kZ, kS, kD, kW, kM} in its input.go. +# the config.ini that replaced json names those same fourteen, in the same order +_BUTTON_NAMES: Final = ( + "up", "down", "left", "right", "a", "b", "c", "x", "y", "z", "start", "d", "w", "menu", +) + +def write_json_config(config_path: Path, resolution: Resolution, /, *, scale_internal_resolution: bool) -> None: + conf: dict[str, object] = {} + + if config_path.is_file(): + try: + with config_path.open() as c: + conf = json.load(c) + except Exception: + # a config we can't read is left alone rather than replaced by one holding + # only our keys: the game's paths to its common files live in there + _logger.warning("%s could not be read, leaving it as it is", config_path) + return + + # Joystick configuration seems completely broken in 0.98.2 Linux + # so let's force keyboad and use a pad2key + conf["KeyConfig"] = Keymapping + conf["JoystickConfig"] = Joymapping + conf["Fullscreen"] = True + + conf["FullscreenWidth"] = resolution["width"] + conf["FullscreenHeight"] = resolution["height"] + + if scale_internal_resolution: + conf["GameWidth"] = resolution["width"] + conf["GameHeight"] = resolution["height"] + + js_out = json.dumps(conf, indent=2) + with ensure_parents_and_open(config_path, "w") as jout: + jout.write(js_out) + +def write_ini_config(config_path: Path, resolution: Resolution, /, *, scale_internal_resolution: bool) -> None: + video = { + "Fullscreen": "1", + "WindowWidth": str(resolution["width"]), + "WindowHeight": str(resolution["height"]), + } + + if scale_internal_resolution: + video["GameWidth"] = str(resolution["width"]) + video["GameHeight"] = str(resolution["height"]) + + wanted: dict[str, dict[str, str]] = {"Video": video} + + for section, mapping in (("Keys", Keymapping), ("Joystick", Joymapping)): + for player, player_config in enumerate(mapping, start=1): + wanted[f"{section}_P{player}"] = { + "Joystick": str(player_config["Joystick"]), + **dict(zip(_BUTTON_NAMES, player_config["Buttons"], strict=True)), + } + + lowered = { + section.lower(): {key.lower(): value for key, value in values.items()} + for section, values in wanted.items() + } + + original = config_path.read_text(encoding="utf-8-sig", errors="replace").splitlines() + + lines: list[str] = [] + values: dict[str, str] | None = None + + for line in original: + stripped = line.strip() + + if stripped.startswith("[") and stripped.endswith("]"): + values = lowered.get(stripped[1:-1].lower()) + elif values and not stripped.startswith(";"): + key, separator, _ = line.partition("=") + if separator and (value := values.get(key.strip().lower())) is not None: + line = f"{key}= {value}" + + lines.append(line) + + config_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + class IkemenGenerator(Generator): + def writesToRom(self, config) -> bool: + return True + def getHotkeysContext(self) -> HotkeysContext: return { "name": "ikemen", "keys": { "exit": ["KEY_LEFTALT", "KEY_F4"], "menu": "KEY_ESC" } } + def running(self, config, rom) -> AbstractContextManager[None]: + if get_windows_exe(rom) is None: + return nullcontext() + + return get_wine_runner().running() + def generate(self, system, rom, playersControllers, metadata, guns, wheels, gameResolution): - config_path = rom / "save" / "config.json" + game_dir = wine.get_game_dir(rom) + game_exe = get_windows_exe(rom) + ikemen_binary = get_linux_binary(game_dir) - try: - with config_path.open() as c: - conf = json.load(c) - except Exception: - conf = {} + save_dir = game_dir / "save" + + scale_internal_resolution = system.config.get_bool("scale_internal_resolution") + + #since December 2024, ikemen use config.ini + if (ini_config := save_dir / "config.ini").is_file(): + write_ini_config(ini_config, gameResolution, scale_internal_resolution=scale_internal_resolution) + elif (json_config := save_dir / "config.json").is_file() or (game_exe is None and ikemen_binary == _IKEMEN_SYSTEM_BINARY): + write_json_config(json_config, gameResolution, scale_internal_resolution=scale_internal_resolution) + else: + _logger.warning( + "%s ships no save/config.ini nor save/config.json, leaving the controls to its own engine", + rom + ) + + if game_exe is None: + environment = { "SDL_GAMECONTROLLERCONFIG": generate_sdl_game_controller_config(playersControllers) } + + return Command.Command(array=[ikemen_binary], env=environment) + + wine_runner = get_wine_runner() + wine_runner.create_bottle() + + wine_runner.install_wine_trick('openal') + wine_runner.install_wine_trick('corefonts') - # Joystick configuration seems completely broken in 0.98.2 Linux - # so let's force keyboad and use a pad2key - conf["KeyConfig"] = Keymapping - conf["JoystickConfig"] = Joymapping - conf["Fullscreen"] = True + environment = wine_runner.get_environment() + environment.update({ + "WINEDLLOVERRIDES": "winemenubuilder.exe=", + }) - js_out = json.dumps(conf, indent=2) - with ensure_parents_and_open(config_path, "w") as jout: - jout.write(js_out) + if Path("/var/tmp/nvidia.prime").exists(): + environment.update({ + "__NV_PRIME_RENDER_OFFLOAD": "1", + "__VK_LAYER_NV_optimus": "NVIDIA_only", + "__GLX_VENDOR_LIBRARY_NAME": "nvidia", + "VK_ICD_FILENAMES": "/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json", + "VK_DRIVER_FILES": "/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json", + "VK_LAYER_PATH": "/usr/share/vulkan/explicit_layer.d" + }) - commandArray = ["/usr/bin/batocera-ikemen", rom] + return Command.Command( + array=wine_runner.game_command(game_exe), + env=environment + ) - return Command.Command(array=commandArray, env={ "SDL_GAMECONTROLLERCONFIG": generate_sdl_game_controller_config(playersControllers) }) + def executionDirectory(self, config, rom): + return wine.get_game_dir(rom) diff --git a/package/batocera/core/batocera-configgen/configs/configgen-defaults.yml b/package/batocera/core/batocera-configgen/configs/configgen-defaults.yml index c89f5017938..112244a8034 100644 --- a/package/batocera/core/batocera-configgen/configs/configgen-defaults.yml +++ b/package/batocera/core/batocera-configgen/configs/configgen-defaults.yml @@ -326,8 +326,6 @@ hurrican: ikemen: emulator: ikemen core: ikemen - options: - videomode: max-720x480 intellivision: emulator: libretro core: freeintv diff --git a/package/batocera/core/batocera-scripts/batocera-scripts.mk b/package/batocera/core/batocera-scripts/batocera-scripts.mk index 9ed1dbdf1ef..1a291dec6ad 100644 --- a/package/batocera/core/batocera-scripts/batocera-scripts.mk +++ b/package/batocera/core/batocera-scripts/batocera-scripts.mk @@ -76,7 +76,6 @@ define BATOCERA_SCRIPTS_INSTALL_TARGET_CMDS install -m 0755 $(BATOCERA_SCRIPTS_PATH)/scripts/batocera-planemode $(TARGET_DIR)/usr/bin/ install -m 0755 $(BATOCERA_SCRIPTS_PATH)/scripts/batocera-switch-screen-checker $(TARGET_DIR)/usr/bin/ install -m 0755 $(BATOCERA_SCRIPTS_PATH)/scripts/batocera-switch-screen-checker-delayed $(TARGET_DIR)/usr/bin/ - install -m 0755 $(BATOCERA_SCRIPTS_PATH)/scripts/batocera-ikemen $(TARGET_DIR)/usr/bin/ install -m 0755 $(BATOCERA_SCRIPTS_PATH)/scripts/batocera-streaming $(TARGET_DIR)/usr/bin/ install -m 0644 $(BATOCERA_SCRIPTS_PATH)/rules/80-switch-screen.rules $(TARGET_DIR)/etc/udev/rules.d mkdir -p $(TARGET_DIR)/etc/udev/rules.d diff --git a/package/batocera/core/batocera-scripts/scripts/batocera-ikemen b/package/batocera/core/batocera-scripts/scripts/batocera-ikemen deleted file mode 100755 index 94321c5a043..00000000000 --- a/package/batocera/core/batocera-scripts/scripts/batocera-ikemen +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -cd "$1" -MESA_GL_VERSION_OVERRIDE=2.1 /usr/bin/ikemen diff --git a/package/batocera/emulationstation/batocera-es-system/es_systems.yml b/package/batocera/emulationstation/batocera-es-system/es_systems.yml index 359d31a0ec7..fa53d47e59d 100644 --- a/package/batocera/emulationstation/batocera-es-system/es_systems.yml +++ b/package/batocera/emulationstation/batocera-es-system/es_systems.yml @@ -2980,7 +2980,7 @@ ikemen: manufacturer: Elecbyte release: 2021 hardware: port - extensions: [ikemen, pc] + extensions: [ikemen, pc, squashfs] emulators: ikemen: ikemen: { requireAnyOf: [BR2_PACKAGE_IKEMEN] } diff --git a/package/batocera/emulators/ikemen/ikemen.emulator.yml b/package/batocera/emulators/ikemen/ikemen.emulator.yml index b9c6b0b9673..46e2229c478 100644 --- a/package/batocera/emulators/ikemen/ikemen.emulator.yml +++ b/package/batocera/emulators/ikemen/ikemen.emulator.yml @@ -12,5 +12,13 @@ shared_features: - bezel.tattoo_corner - bezel.tattoo_file - bezel.resize_tattoo +custom_features: + scale_internal_resolution: + submenu: VIDEO + prompt: SCALE INTERNAL RESOLUTION + description: Render the game at the screen resolution rather than the internal one its motif was drawn for, which stretches it to the screen's aspect. + choices: + "OFF": false + "ON": true systems: - ikemen