Skip to content

Commit 1663abd

Browse files
committed
Add batocera-launch emulator launcher framework
Introduce new Python packages: - batocera-launch: A new async-based emulator launcher framework with modules for configuration loading, device management (controllers, evmapy, guns, hotkeys, mouse, video, wheels), emulator abstraction with plugin discovery via entry points, command execution, EPIPE-tolerant logging, and shared path constants. - batocera-launch-flycast: Flycast emulator plugin for the new launcher framework. All packages are registered as workspace members in pyproject.toml and uv.lock. batocera-common and batoceara-launch have been added as buildroot packages with appropriate dependencies. configgen and batocera-es-system have been updated to depend on the new launcher framework.
1 parent a8a75ef commit 1663abd

342 files changed

Lines changed: 35998 additions & 183 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Config.in

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,3 +740,22 @@ menu "Database"
740740
endmenu
741741

742742
endmenu
743+
744+
# Include hidden batocera-launch packages
745+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch/Config.in"
746+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-fallout/Config.in"
747+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-cdogs/Config.in"
748+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-cgenius/Config.in"
749+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-drastic/Config.in"
750+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-flycast/Config.in"
751+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-kodi/Config.in"
752+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-libretro/Config.in"
753+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-libretro-fbneo/Config.in"
754+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-libretro-scummvm/Config.in"
755+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-mame/Config.in"
756+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-mame-common/Config.in"
757+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-openjazz/Config.in"
758+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-openjk/Config.in"
759+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-openjkdf2/Config.in"
760+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-openmohaa/Config.in"
761+
source "$BR2_EXTERNAL_BATOCERA_PATH/python-src/batocera-launch-rpcs3/Config.in"

package/batocera/core/batocera-configgen/Config.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ config BR2_PACKAGE_BATOCERA_CONFIGGEN
22
bool "batocera configgen"
33
select BR2_PACKAGE_PYTHON3
44
select BR2_PACKAGE_PYTHON_BATOCERA_COMMON
5+
select BR2_PACKAGE_BATOCERA_LAUNCH
56
select BR2_PACKAGE_PYTHON_PYYAML
67
select BR2_PACKAGE_PYTHON_RUAMEL_YAML
78
select BR2_PACKAGE_PYTHON_TOML

package/batocera/core/batocera-configgen/batocera-configgen.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ BATOCERA_CONFIGGEN_SOURCE=
99
BATOCERA_CONFIGGEN_SETUP_TYPE = hatch
1010
BATOCERA_CONFIGGEN_DEPENDENCIES = \
1111
python-batocera-common \
12+
batocera-launch \
1213
python-toml \
1314
python-evdev \
1415
python-pyudev \

package/batocera/core/batocera-configgen/configgen/configgen/batoceraPaths.py

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
from __future__ import annotations
22

33
from contextlib import contextmanager
4-
from pathlib import Path
5-
from typing import IO, TYPE_CHECKING, Any, Final, overload
6-
7-
if TYPE_CHECKING:
8-
from _typeshed import OpenBinaryModeUpdating, OpenBinaryModeWriting, OpenTextModeUpdating, OpenTextModeWriting
9-
from collections.abc import Generator
10-
from io import BufferedRandom, BufferedWriter, TextIOWrapper
4+
from typing import IO, TYPE_CHECKING, Any, overload
115

126
from batocera_common.paths import (
137
BATOCERA_CONF as BATOCERA_CONF,
@@ -25,40 +19,34 @@
2519
SCREENSHOTS as SCREENSHOTS,
2620
USERDATA as USERDATA,
2721
)
22+
from batocera_launch.paths import (
23+
BATOCERA_ES_DIR as BATOCERA_ES_DIR,
24+
BATOCERA_SHADERS as BATOCERA_SHADERS,
25+
CONF_INIT as CONF_INIT,
26+
CONFIGGEN_DATA_DIR as CONFIGGEN_DATA_DIR,
27+
DATAINIT_DIR as DATAINIT_DIR,
28+
DEFAULTS_DIR as DEFAULTS_DIR,
29+
ES_GAMES_METADATA as ES_GAMES_METADATA,
30+
ES_GUNS_ART_METADATA as ES_GUNS_ART_METADATA,
31+
ES_GUNS_METADATA as ES_GUNS_METADATA,
32+
ES_SETTINGS as ES_SETTINGS,
33+
ES_WHEELS_METADATA as ES_WHEELS_METADATA,
34+
EVMAPY as EVMAPY,
35+
HOME_INIT as HOME_INIT,
36+
SYSTEM_DECORATIONS as SYSTEM_DECORATIONS,
37+
SYSTEM_SCRIPTS as SYSTEM_SCRIPTS,
38+
USER_DECORATIONS as USER_DECORATIONS,
39+
USER_ES_DIR as USER_ES_DIR,
40+
USER_SCRIPTS as USER_SCRIPTS,
41+
USER_SHADERS as USER_SHADERS,
42+
configure_emulator as configure_emulator,
43+
)
2844

29-
DATAINIT_DIR: Final = BATOCERA_SHARE_DIR / 'datainit'
30-
31-
HOME_INIT: Final = DATAINIT_DIR / 'system'
32-
CONF_INIT: Final = HOME_INIT / 'configs'
33-
34-
EVMAPY: Final = CONFIGS / 'evmapy'
35-
36-
USER_ES_DIR: Final = CONFIGS / 'emulationstation'
37-
BATOCERA_ES_DIR: Final = Path('/usr/share/emulationstation')
38-
CONFIGGEN_DATA_DIR: Final = Path('/usr/share/batocera/configgen/data')
39-
40-
_ES_RESOURCES_DIR: Final = BATOCERA_ES_DIR / 'resources'
41-
42-
ES_SETTINGS: Final = USER_ES_DIR / 'es_settings.cfg'
43-
ES_GUNS_METADATA: Final = _ES_RESOURCES_DIR / 'gungames.xml'
44-
ES_WHEELS_METADATA: Final = _ES_RESOURCES_DIR / 'wheelgames.xml'
45-
ES_GAMES_METADATA: Final = _ES_RESOURCES_DIR / 'gamesdb.xml'
46-
ES_GUNS_ART_METADATA: Final = CONFIGGEN_DATA_DIR / 'gamesbuttonsdb.xml'
47-
48-
DEFAULTS_DIR: Final = BATOCERA_SHARE_DIR / 'configgen'
49-
50-
USER_SHADERS: Final = USERDATA / 'shaders'
51-
BATOCERA_SHADERS: Final = BATOCERA_SHARE_DIR / 'shaders'
52-
53-
USER_DECORATIONS: Final = USERDATA / 'decorations'
54-
SYSTEM_DECORATIONS: Final = DATAINIT_DIR / 'decorations'
55-
56-
USER_SCRIPTS: Final = HOME / 'scripts'
57-
SYSTEM_SCRIPTS: Final = DEFAULTS_DIR / 'scripts'
58-
59-
60-
def configure_emulator(rom: Path, /) -> bool:
61-
return str(rom) == 'config'
45+
if TYPE_CHECKING:
46+
from _typeshed import OpenBinaryModeUpdating, OpenBinaryModeWriting, OpenTextModeUpdating, OpenTextModeWriting
47+
from collections.abc import Generator
48+
from io import BufferedRandom, BufferedWriter, TextIOWrapper
49+
from pathlib import Path
6250

6351

6452
def mkdir_if_not_exists(dir: Path, /) -> None:
Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,3 @@
11
from __future__ import annotations
22

3-
from collections.abc import Iterator, Mapping
4-
from dataclasses import dataclass, replace
5-
from typing import TYPE_CHECKING, Self, TypedDict, Unpack, cast
6-
7-
if TYPE_CHECKING:
8-
import xml.etree.ElementTree as ET
9-
10-
11-
class _InputChanges(TypedDict, total=False):
12-
name: str
13-
type: str
14-
id: str
15-
value: str
16-
code: str | None
17-
18-
19-
@dataclass(slots=True, kw_only=True)
20-
class Input:
21-
name: str
22-
type: str
23-
id: str
24-
value: str
25-
code: str | None = None
26-
27-
def replace(self, /, **changes: Unpack[_InputChanges]) -> Self:
28-
return replace(self, **changes)
29-
30-
@classmethod
31-
def from_element(cls, element: ET.Element, /) -> Self:
32-
return cls(
33-
name=cast("str", element.get("name")),
34-
type=cast("str", element.get("type")),
35-
id=cast("str", element.get("id")),
36-
value=cast("str", element.get("value")),
37-
code=element.get("code")
38-
)
39-
40-
@classmethod
41-
def from_parent_element(cls, parent_element: ET.Element, /) -> Iterator[tuple[str, Self]]:
42-
for element in parent_element.iterfind('./input'):
43-
input = cls.from_element(element)
44-
yield input.name, input
45-
46-
47-
type InputMapping = Mapping[str, Input]
48-
type InputDict = dict[str, Input]
3+
from batocera_launch.devices.input import Input as Input, InputDict as InputDict, InputMapping as InputMapping
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from __future__ import annotations
2+
3+
from contextlib import asynccontextmanager
4+
from dataclasses import asdict, dataclass
5+
from typing import TYPE_CHECKING, Self
6+
7+
from batocera_launch import (
8+
Command,
9+
Emulator,
10+
HotkeysContext,
11+
SystemConfig,
12+
cached_dataclass,
13+
cached_property,
14+
)
15+
16+
from .config import Config as _ConfiggenConfig, SystemConfig as _ConfiggenSystemConfig
17+
from .Emulator import Emulator as _ConfiggenEmulator
18+
from .generators.importer import get_generator
19+
20+
if TYPE_CHECKING:
21+
from argparse import Namespace
22+
from collections.abc import AsyncGenerator
23+
from pathlib import Path
24+
25+
from batocera_launch.devices.device import DeviceInfo
26+
from configgen.generators.Generator import Generator
27+
from configgen.types import DeviceInfo as _ConfiggenDeviceInfo
28+
29+
30+
def _convert_device_info(device_info: DeviceInfo, /) -> _ConfiggenDeviceInfo:
31+
result: _ConfiggenDeviceInfo = {
32+
'eventId': device_info.event_id,
33+
'sysfs_path': device_info.sysfs_path,
34+
'isJoystick': device_info.is_joystick,
35+
'isWheel': device_info.is_wheel,
36+
'isMouse': device_info.is_mouse,
37+
'associatedDevices': device_info.associated_devices,
38+
'joystick_index': device_info.joystick_index,
39+
'mouse_index': device_info.mouse_index,
40+
}
41+
42+
if device_info.wheel_rotation is not None:
43+
result['wheel_rotation'] = device_info.wheel_rotation
44+
45+
return result
46+
47+
48+
@dataclass(slots=True)
49+
class ConfiggenEmulator(_ConfiggenEmulator):
50+
system_config: SystemConfig
51+
52+
def __post_init__(self, args: Namespace, rom: Path, /) -> None:
53+
self.name = args.system
54+
self.game_info_xml = str(args.gameinfoxml)
55+
self.config = _ConfiggenSystemConfig(dict(self.system_config.data))
56+
self.renderconfig = _ConfiggenConfig(dict(self.system_config.render_config.data))
57+
58+
59+
@cached_dataclass
60+
class Configgen(Emulator):
61+
generator: Generator
62+
configgen_emulator: ConfiggenEmulator
63+
64+
@cached_property
65+
def hotkeygen_context(self) -> HotkeysContext:
66+
return self.generator.getHotkeysContext()
67+
68+
@property
69+
def execution_path(self) -> Path | None:
70+
return self.generator.executionDirectory(self.configgen_emulator.config, self.rom)
71+
72+
@property
73+
def video_mode(self) -> str:
74+
return self.generator.getResolutionMode(self.configgen_emulator.config)
75+
76+
@property
77+
def needs_mouse(self) -> bool:
78+
return self.generator.getMouseMode(self.configgen_emulator.config, self.rom)
79+
80+
@property
81+
def handles_bezels(self) -> bool:
82+
return self.generator.supportsInternalBezels()
83+
84+
@property
85+
def handles_hud(self) -> bool:
86+
return self.generator.hasInternalMangoHUDCall()
87+
88+
@property
89+
def needs_overlayfs(self) -> bool:
90+
return self.generator.writesToRom(self.configgen_emulator.config)
91+
92+
@cached_property
93+
def in_game_ratio(self) -> float:
94+
return self.generator.getInGameRatio(
95+
self.configgen_emulator.config,
96+
{
97+
'width': self.resolution.width,
98+
'height': self.resolution.height,
99+
},
100+
self.rom,
101+
)
102+
103+
@cached_property
104+
def guns_borders_size(self) -> str | None:
105+
return self.configgen_emulator.guns_borders_size_name(self.guns) # pyright: ignore[reportArgumentType]
106+
107+
async def configure(self) -> Command:
108+
configgen_command = self.generator.generate(
109+
self.configgen_emulator,
110+
self.rom,
111+
self.controllers, # pyright: ignore
112+
self.metadata,
113+
self.guns, # pyright: ignore
114+
{key: _convert_device_info(wheel) for key, wheel in self.wheels.items()},
115+
asdict(self.resolution), # pyright: ignore
116+
)
117+
118+
return Command(
119+
configgen_command.array,
120+
configgen_command.env,
121+
)
122+
123+
@classmethod
124+
@asynccontextmanager
125+
async def prepare_emulator(cls, args: Namespace, max_players: int, /) -> AsyncGenerator[Self]:
126+
system_config = SystemConfig.load(args)
127+
128+
generator = get_generator(system_config.emulator, system_config.core)
129+
configgen_emulator = ConfiggenEmulator(args, args.rom, system_config)
130+
emulator = cls(
131+
args.system, args.systemname, system_config, args.gameinfoxml, generator, configgen_emulator
132+
)
133+
134+
async with emulator._prepare_devices_and_data(args, max_players) as emulator:
135+
yield emulator

package/batocera/core/batocera-configgen/configgen/pyproject.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
[project]
22
name = "batocera-configgen"
3-
version = '1.5'
3+
version = '44.0'
44
dependencies = [
5-
'batocera-common',
6-
'PyYAML',
5+
'batocera-launch',
76
'toml',
87
'evdev; sys_platform == "linux"',
98
'pyudev; sys_platform == "linux"',
109
'configobj',
1110
'ffmpeg-python',
1211
'pillow',
13-
'ruamel.yaml',
1412
'requests',
1513
'qrcode',
1614
'pysdl2',
@@ -19,6 +17,9 @@ dependencies = [
1917
[project.scripts]
2018
emulatorlauncher = "configgen.emulatorlauncher:launch"
2119

20+
[project.entry-points."batocera_launch.emulators"]
21+
configgen = 'configgen.launch:Configgen'
22+
2223
[build-system]
2324
requires = ["hatchling"]
2425
build-backend = "hatchling.build"

package/batocera/core/batocera-controlcenter/controlcenter.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
<text display="${curl http://127.0.0.1:1234/runningGame | jq -r '.name' | sed 's/null//'}" align="left" refresh="3" if="!id(gamemarquee)" />
7373
</feature>
7474
<feature>
75-
<text display='${P=$(ps -o etime= -C emulatorlauncher 2>/dev/null | head -1 | tr -d [[:blank:]]) &amp;&amp; [ -n "$P" ] &amp;&amp; echo "Playing for $P"}' align="left" refresh="1" id="playtime" />
75+
<text display='${PID=$(pgrep -n -x batocera-launch) &amp;&amp; P=$(ps -o etime= -p "$PID" 2>/dev/null | tr -d "[[:blank:]]") &amp;&amp; [ -n "$P" ] &amp;&amp; echo "Playing for $P"}' align="left" refresh="1" id="playtime" />
7676
</feature>
7777
</hgroup>
7878
<vgroup>

package/batocera/core/batocera-desktopapps/scripts/batocera-config-cemu

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ then
55
export DISPLAY=$(getLocalXDisplay)
66
fi
77

8-
emulatorlauncher -system wiiu -rom config
8+
batocera-launch -system wiiu -rom config
99

1010
exit 0

package/batocera/core/batocera-desktopapps/scripts/batocera-config-model2emu

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ else
77
export DISPLAY=$(getLocalXDisplay)
88
fi
99

10-
emulatorlauncher -system model2 -rom config
10+
batocera-launch -system model2 -rom config
1111

1212
exit 0

0 commit comments

Comments
 (0)