Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,15 @@

### import always needed ###
import argparse
import ctypes
import json
import logging
import os
import signal
import subprocess
import threading
import time
from copy import deepcopy
from pathlib import Path
from sys import exit
from typing import TYPE_CHECKING, Any, cast

import pyudev
import sdl2
from typing import TYPE_CHECKING

from .batoceraPaths import BATOCERA_SHARE_DIR, ES_GAMES_METADATA, SAVES, SYSTEM_SCRIPTS, USER_SCRIPTS
from .controller import Controller
Expand All @@ -33,7 +27,7 @@
from .gun import Gun
from .utils import bezels as bezelsUtil, metadata, videoMode, wheelsUtils
from .utils.evmapy import evmapy
from .utils.hotkeygen import set_hotkeygen_context
from .utils.hotkeygen import HotkeygenManager
from .utils.logger import setup_logging
from .utils.squashfs import squashfs_rom

Expand All @@ -47,13 +41,6 @@

_logger = logging.getLogger(__name__)

# A lock to safely modify the active controller list from multiple threads
_player_controllers_lock = threading.Lock()
# A global variable to hold the current, up-to-date list of player controllers
_active_player_controllers = []
# Global reference to the evmapy configurator instance
_evmapy_instance = None

def main(args: argparse.Namespace, maxnbplayers: int) -> int:
# squashfs roms if squashed
if args.rom.suffix == ".squashfs":
Expand All @@ -63,16 +50,10 @@ def main(args: argparse.Namespace, maxnbplayers: int) -> int:
return start_rom(args, maxnbplayers, args.rom, args.rom)

def start_rom(args: argparse.Namespace, maxnbplayers: int, rom: Path, original_rom: Path) -> int:
global _active_player_controllers, _evmapy_instance

player_controllers = Controller.load_for_players(maxnbplayers, args)

# Initialize the global state with the initial controller list
with _player_controllers_lock:
_active_player_controllers = list(player_controllers)

# Start the background monitor thread.
monitor_thread = threading.Thread(target=_controller_monitor_thread, daemon=True)
# Keep a copy of this list to pass to evmapy when we start monitoring for new controllers
active_controllers = list(player_controllers)

# find the system to run
systemName: str = args.system
Expand Down Expand Up @@ -157,10 +138,9 @@ def start_rom(args: argparse.Namespace, maxnbplayers: int, rom: Path, original_r
callExternalScripts(USER_SCRIPTS, "gameStart", [systemName, system.config.emulator, effectiveCore, rom])

# run the emulator
_evmapy_instance = evmapy(systemName, system.config.emulator, effectiveCore, original_rom, player_controllers, guns)
with (
_evmapy_instance,
set_hotkeygen_context(generator, system)
evmapy(systemName, system.config.emulator, effectiveCore, original_rom, player_controllers, guns) as evmapy_instance,
HotkeygenManager(generator, system) as hotkeygen_manager
):
# change directory if wanted
executionDirectory = generator.executionDirectory(system.config, rom)
Expand Down Expand Up @@ -198,6 +178,7 @@ def start_rom(args: argparse.Namespace, maxnbplayers: int, rom: Path, original_r
gun_border_size_name = system.guns_borders_size_name(guns)
if gun_border_size_name is not None:
_logger.debug("using configgen internal gun borders for emulator %s", system.config.emulator)

from .utils.gun_borders import draw_gun_borders
draw_gun_borders(
gun_border_size_name,
Expand All @@ -209,12 +190,8 @@ def start_rom(args: argparse.Namespace, maxnbplayers: int, rom: Path, original_r
_logger.error(e)

with profiler.pause():
try:
_logger.debug("Triggering mouse reset to primary display")
subprocess.call(["/usr/bin/hotkeygen", "--reset-mouse"])
except Exception as e:
_logger.warning("Failed to reset mouse: %s", e)
monitor_thread.start()
hotkeygen_manager.reset_mouse()
evmapy_instance.start_monitoring_controllers(active_controllers)
exitCode = runCommand(cmd)

# run a script after emulator shuts down
Expand Down Expand Up @@ -444,123 +421,6 @@ def getHudConfig(system: Emulator, systemName: str, emulator: str, core: str, ro
configstr = configstr.replace("%EMULATORCORE%", hudConfig_protectStr(emulatorstr))
return configstr.replace("%THUMBNAIL%", hudConfig_protectStr(gameThumbnail))

def _reconfigure_evmapy_on_the_fly():
# Re-runs the evmapy configuration by creating a NEW evmapy instance with the latest controller list.
global _evmapy_instance, _active_player_controllers

with _player_controllers_lock:
if not _evmapy_instance:
return

_logger.info(">>> --- STARTING EVMAPY RECONFIGURATION ---")

valid_controllers = [c for c in _active_player_controllers if c is not None]
_logger.info(">>> Found %s valid controllers to configure.", len(valid_controllers))
for c in valid_controllers:
_logger.info(">>> - Configuring P%s with Path: %s", c.player_number, c.device_path)

new_evmapy_instance = evmapy(
system=_evmapy_instance.system,
emulator=_evmapy_instance.emulator,
core=_evmapy_instance.core,
rom=_evmapy_instance.rom,
controllers=deepcopy(valid_controllers),
guns=_evmapy_instance.guns
)

_evmapy_instance = new_evmapy_instance

subprocess.call(['batocera-evmapy', 'stop'])
time.sleep(0.5)
cast('Any', _evmapy_instance)._evmapy__prepare()
subprocess.call(['batocera-evmapy', 'start'])

_logger.info(">>> --- EVMAPY RECONFIGURATION COMPLETE ---")


def _controller_monitor_thread():
# Runs in the background, watching for controller add/remove events.
# Uses pysdl2 to reliably get controller GUIDs and paths, then intelligently "revives"
# the original controller object to preserve player order without disrupting the emulator.
global _active_player_controllers

initial_controllers_snapshot = []
with _player_controllers_lock:
initial_controllers_snapshot = deepcopy(_active_player_controllers)
for i, p_controller in enumerate(initial_controllers_snapshot):
if p_controller and p_controller.guid:
_logger.info(">>> [P%s] Stored GUID: %s, Initial Path: %s", i+1, p_controller.guid, p_controller.device_path)

we_initialized_sdl = False
try:
if sdl2.SDL_WasInit(sdl2.SDL_INIT_JOYSTICK) == 0:
_logger.info(">>> SDL2 joystick subsystem not initialized. Initializing it now.")
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK)
we_initialized_sdl = True
else:
_logger.info(">>> SDL2 joystick subsystem already initialized by host (emulator). Will not re-initialize.")
except Exception as e:
_logger.error("FATAL: Could not initialize pysdl2 for controller monitoring: %s", e)
return

context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem='input')

_logger.info(">>> Starting background controller monitor.")
for device in iter(monitor.poll, None):
if device.properties.get('ID_INPUT_JOYSTICK') != '1':
continue

_logger.info("--- Joystick Event Detected: %s on %s ---", device.action, device.sys_path)
reconfigure_needed = False

sdl2.SDL_JoystickUpdate()
online_controllers_map = {}
for i in range(sdl2.SDL_NumJoysticks()):
try:
guid_struct = sdl2.SDL_JoystickGetDeviceGUID(i)
guid_str_buffer = (ctypes.c_char * 33)()
sdl2.SDL_JoystickGetGUIDString(guid_struct, guid_str_buffer, 33)
guid = guid_str_buffer.value.decode('utf-8')

path_bytes = sdl2.SDL_JoystickPathForIndex(i)
path = path_bytes.decode('utf-8') if path_bytes else None

if guid and path:
online_controllers_map[guid] = path
except Exception as e:
_logger.warning("Error while querying joystick index %s with pysdl2: %s", i, e)

_logger.info(">>> [Check 1] Pysdl2 scan found online controllers: %s", online_controllers_map)

with _player_controllers_lock:
new_active_controllers: list[Controller | None] = [None] * len(initial_controllers_snapshot)

for i, initial_controller in enumerate(initial_controllers_snapshot):
if initial_controller and initial_controller.guid in online_controllers_map:
new_path = online_controllers_map[initial_controller.guid]
if initial_controller.device_path != new_path:
_logger.info(">>> [Revival] Player %s (GUID: %s) path has changed.", initial_controller.player_number, initial_controller.guid)
initial_controller.device_path = new_path
new_active_controllers[i] = initial_controller

current_paths = [c.device_path if c else None for c in _active_player_controllers]
new_paths = [c.device_path if c else None for c in new_active_controllers]

if current_paths != new_paths:
_logger.info(">>> [Check 2] Controller state changed. Old Paths: %s. New Paths: %s", current_paths, new_paths)
_active_player_controllers = new_active_controllers
reconfigure_needed = True
else:
_logger.info(">>> [Check 2] No change in assigned controller paths detected.")

if reconfigure_needed:
time.sleep(1)
_reconfigure_evmapy_on_the_fly()

if we_initialized_sdl:
sdl2.SDL_QuitSubSystem(sdl2.SDL_INIT_JOYSTICK)

def runCommand(command: Command) -> int:
global proc
Expand Down
Loading