From 2f036f3b5e8c5110490291f465a4ff5c6418c54b Mon Sep 17 00:00:00 2001 From: jacobmix Date: Thu, 16 Oct 2025 00:03:31 +0200 Subject: [PATCH 01/13] Dynamic PINE port test --- Rac2Client.py | 40 +++++++++++++++++++++++++++++++++++++++- Rac2Interface.py | 10 +++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index bead15e..49b54cc 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -7,6 +7,7 @@ import os import subprocess import traceback +import socket from CommonClient import ClientCommandProcessor, CommonContext, get_base_parser, logger, server_loop, gui_enabled from NetUtils import ClientStatus @@ -20,6 +21,44 @@ from .ClientReceiveItems import handle_received_items from .NotificationManager import NotificationManager from .Rac2Interface import HUD_MESSAGE_DURATION, ConnectionState, Rac2Interface, Rac2Planet +from configparser import ConfigParser +from .Rac2Interface import create_pine_interface, Rac2Interface + + +def find_free_port(start=28021, end=28031): + for port in range(start, end + 1): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(("127.0.0.1", port)) + return port + except OSError: + continue + return 28011 + +def set_pine_port(ini_path, port): + config = ConfigParser() + config.read(ini_path) + if 'EmuCore' in config and 'PINESlot' in config['EmuCore']: + config['EmuCore']['PINESlot'] = str(port) + with open(ini_path, 'w') as f: + config.write(f) + +def setup_pine(): + """Determine port and create Pine instance early""" + host_settings = get_settings() + game_ini = host_settings.get('rac2_options', {}).get('game_ini') + + if game_ini and os.path.exists(game_ini): + port = find_free_port() + set_pine_port(game_ini, port) + else: + port = 28011 + + create_pine_interface(port) + return port + +# Run early so Pine instance exists for Rac2Interface +pine_port = setup_pine() class Rac2CommandProcessor(ClientCommandProcessor): @@ -294,7 +333,6 @@ def get_pcsx2_crc(iso_path: str) -> Optional[int]: return crc - def launch(): Utils.init_logging("RAC2 Client") diff --git a/Rac2Interface.py b/Rac2Interface.py index aac47b7..ca210ab 100644 --- a/Rac2Interface.py +++ b/Rac2Interface.py @@ -452,10 +452,16 @@ def planet_by_id(planet_id) -> Optional[Rac2Planet]: # current_amount: int # current_capacity: int +pcsx2_interface: Pine | None = None + +def create_pine_interface(slot: int = 28011): + global pcsx2_interface + pcsx2_interface = Pine(slot=slot) class Rac2Interface: """Interface sitting in front of the pcsx2_interface to provide higher level functions for interacting with RAC2""" - pcsx2_interface: Pine = Pine() + # Pass the dynamic port if available + pcsx2_interface: Pine | None = None # This will be set via create_pine_interface() addresses: Addresses = None vendor: Vendor = None connection_status: str @@ -469,6 +475,8 @@ class Rac2Interface: def __init__(self, logger) -> None: self.logger = logger self.vendor = Vendor(self) + global pcsx2_interface + self.pcsx2_interface = pcsx2_interface def give_equipment_to_player(self, equipment: EquipmentData): if isinstance(equipment, WeaponData) and equipment.base_weapon_offset is not None: From 468e12ed14b7b44afaf4e65cfa0768c600ab8850 Mon Sep 17 00:00:00 2001 From: jacobmix Date: Thu, 16 Oct 2025 03:40:24 +0200 Subject: [PATCH 02/13] Update Rac2Client.py --- Rac2Client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index 49b54cc..2915cad 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -20,9 +20,8 @@ from .Callbacks import update, init from .ClientReceiveItems import handle_received_items from .NotificationManager import NotificationManager -from .Rac2Interface import HUD_MESSAGE_DURATION, ConnectionState, Rac2Interface, Rac2Planet +from .Rac2Interface import HUD_MESSAGE_DURATION, ConnectionState, create_pine_interface, Rac2Interface, Rac2Planet from configparser import ConfigParser -from .Rac2Interface import create_pine_interface, Rac2Interface def find_free_port(start=28021, end=28031): From afdb058cc405f94c8f294aa9c85a3500d39361f4 Mon Sep 17 00:00:00 2001 From: jacobmix Date: Thu, 16 Oct 2025 15:40:36 +0200 Subject: [PATCH 03/13] Update Rac2Client.py Create game ini if path is valid but file missing. Add game ini options if missing. --- Rac2Client.py | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index 2915cad..97e6eb6 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -25,6 +25,7 @@ def find_free_port(start=28021, end=28031): + """Find free port for PINE""" for port in range(start, end + 1): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: try: @@ -32,30 +33,52 @@ def find_free_port(start=28021, end=28031): return port except OSError: continue - return 28011 + return 28011 # fallback default -def set_pine_port(ini_path, port): + +def ensure_pine_settings(ini_path: str, port: int = 28011): + """Ensure INI has [EmuCore] with enablepine and pineslot set correctly""" config = ConfigParser() - config.read(ini_path) - if 'EmuCore' in config and 'PINESlot' in config['EmuCore']: - config['EmuCore']['PINESlot'] = str(port) + config.optionxform = str # preserve key case + + # If INI file missing but path valid → create a minimal one + if not os.path.exists(ini_path) and os.path.isdir(os.path.dirname(ini_path)): with open(ini_path, 'w') as f: - config.write(f) + f.write("[EmuCore]\n") + + config.read(ini_path) + + # --- EmuCore section --- + if 'EmuCore' not in config: + config['EmuCore'] = {} + config['EmuCore']['enablepine'] = 'true' + config['EmuCore']['PINESlot'] = str(port) + + # --- Achievements section --- + if 'Achievements' not in config: + config['Achievements'] = {} + config['Achievements']['enabled'] = 'false' + + # Write back to disk + with open(ini_path, 'w') as f: + config.write(f) + def setup_pine(): """Determine port and create Pine instance early""" host_settings = get_settings() game_ini = host_settings.get('rac2_options', {}).get('game_ini') - if game_ini and os.path.exists(game_ini): + if game_ini and os.path.exists(os.path.dirname(game_ini)): port = find_free_port() - set_pine_port(game_ini, port) + ensure_pine_settings(game_ini, port) else: port = 28011 create_pine_interface(port) return port + # Run early so Pine instance exists for Rac2Interface pine_port = setup_pine() From d3590d2aef8e96d380c4179d8fceb5194457e8c6 Mon Sep 17 00:00:00 2001 From: jacobmix Date: Thu, 16 Oct 2025 17:13:21 +0200 Subject: [PATCH 04/13] Update Rac2Client.py Change copied game ini instead of one selected in host.yaml Also changed "PINESlot" to "pineslot". --- Rac2Client.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index 97e6eb6..df79de8 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -35,35 +35,37 @@ def find_free_port(start=28021, end=28031): continue return 28011 # fallback default - def ensure_pine_settings(ini_path: str, port: int = 28011): - """Ensure INI has [EmuCore] with enablepine and pineslot set correctly""" + """Ensure INI has [EmuCore] and [Achievements] configured for Pine""" config = ConfigParser() config.optionxform = str # preserve key case - - # If INI file missing but path valid → create a minimal one - if not os.path.exists(ini_path) and os.path.isdir(os.path.dirname(ini_path)): + + ini_dir = os.path.dirname(ini_path) + if not os.path.exists(ini_dir): + os.makedirs(ini_dir, exist_ok=True) + + # Create a minimal INI file if it doesn't exist but the directory is valid + if not os.path.exists(ini_path): with open(ini_path, 'w') as f: f.write("[EmuCore]\n") - + config.read(ini_path) # --- EmuCore section --- if 'EmuCore' not in config: config['EmuCore'] = {} config['EmuCore']['enablepine'] = 'true' - config['EmuCore']['PINESlot'] = str(port) + config['EmuCore']['pineslot'] = str(port) # --- Achievements section --- if 'Achievements' not in config: config['Achievements'] = {} config['Achievements']['enabled'] = 'false' - # Write back to disk + # Write updated config with open(ini_path, 'w') as f: config.write(f) - def setup_pine(): """Determine port and create Pine instance early""" host_settings = get_settings() @@ -78,7 +80,6 @@ def setup_pine(): create_pine_interface(port) return port - # Run early so Pine instance exists for Rac2Interface pine_port = setup_pine() @@ -314,6 +315,7 @@ async def patch_and_run_game(aprac2_file: str): base_name = os.path.splitext(aprac2_file)[0] output_path = base_name + '.iso' + # --- Always determine CRC + version once the ISO exists or is created --- if not os.path.exists(output_path): from .PatcherUI import PatcherUI patcher = PatcherUI(aprac2_file, output_path, logger) @@ -328,7 +330,11 @@ async def patch_and_run_game(aprac2_file: str): if version and crc: file_name = f"{version}_{crc:X}.ini" file_path = os.path.join(os.path.dirname(game_ini_path), file_name) + + # Always ensure up-to-date settings (even if ISO already existed) shutil.copy(game_ini_path, file_path) + ensure_pine_settings(file_path, pine_port) + logger.info(f"Configured PINE on port {pine_port} in {os.path.basename(file_path)}") Utils.async_start(run_game(output_path)) From 58cfdcb44293ec38827461275b8aa2dedbb603e7 Mon Sep 17 00:00:00 2001 From: jacobmix Date: Thu, 16 Oct 2025 17:26:52 +0200 Subject: [PATCH 05/13] Update Rac2Client.py Capitalized game ini settings: [EmuCore] EnablePINE = true PINESlot = 28011 [Achievements] Enabled = false --- Rac2Client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index df79de8..0656659 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -54,13 +54,13 @@ def ensure_pine_settings(ini_path: str, port: int = 28011): # --- EmuCore section --- if 'EmuCore' not in config: config['EmuCore'] = {} - config['EmuCore']['enablepine'] = 'true' - config['EmuCore']['pineslot'] = str(port) + config['EmuCore']['EnablePINE'] = 'true' + config['EmuCore']['PINESlot'] = str(port) # --- Achievements section --- if 'Achievements' not in config: config['Achievements'] = {} - config['Achievements']['enabled'] = 'false' + config['Achievements']['Enabled'] = 'false' # Write updated config with open(ini_path, 'w') as f: From efa099b73111d0eee94b93e3ec6c8f81cf111f89 Mon Sep 17 00:00:00 2001 From: jacobmix Date: Thu, 16 Oct 2025 17:36:56 +0200 Subject: [PATCH 06/13] Update Rac2Client.py Make sure game settings ini works with or without capitalization. --- Rac2Client.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index 0656659..c054f50 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -36,15 +36,15 @@ def find_free_port(start=28021, end=28031): return 28011 # fallback default def ensure_pine_settings(ini_path: str, port: int = 28011): - """Ensure INI has [EmuCore] and [Achievements] configured for Pine""" + """Ensure INI has configuration for PINE.""" config = ConfigParser() - config.optionxform = str # preserve key case + config.optionxform = str # Preserve key case exactly ini_dir = os.path.dirname(ini_path) if not os.path.exists(ini_dir): os.makedirs(ini_dir, exist_ok=True) - # Create a minimal INI file if it doesn't exist but the directory is valid + # Create minimal INI if missing if not os.path.exists(ini_path): with open(ini_path, 'w') as f: f.write("[EmuCore]\n") @@ -54,20 +54,31 @@ def ensure_pine_settings(ini_path: str, port: int = 28011): # --- EmuCore section --- if 'EmuCore' not in config: config['EmuCore'] = {} + # Normalize capitalization + for key in list(config['EmuCore'].keys()): + if key.lower() == 'enablepine' and key != 'EnablePINE': + config['EmuCore']['EnablePINE'] = config['EmuCore'].pop(key) + elif key.lower() == 'pineslot' and key != 'PINESlot': + config['EmuCore']['PINESlot'] = config['EmuCore'].pop(key) + # Ensure required settings exist and are correct config['EmuCore']['EnablePINE'] = 'true' config['EmuCore']['PINESlot'] = str(port) # --- Achievements section --- if 'Achievements' not in config: config['Achievements'] = {} + # Normalize capitalization + for key in list(config['Achievements'].keys()): + if key.lower() == 'enabled' and key != 'Enabled': + config['Achievements']['Enabled'] = config['Achievements'].pop(key) config['Achievements']['Enabled'] = 'false' - # Write updated config + # Write updated config back with open(ini_path, 'w') as f: config.write(f) def setup_pine(): - """Determine port and create Pine instance early""" + """Determine port and create Pine instance""" host_settings = get_settings() game_ini = host_settings.get('rac2_options', {}).get('game_ini') From cbeb39c14cedd166edfb9dae5ff352b4de799eca Mon Sep 17 00:00:00 2001 From: jacobmix Date: Thu, 16 Oct 2025 17:52:49 +0200 Subject: [PATCH 07/13] Update Rac2Client.py Keep original ini untouched. --- Rac2Client.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index c054f50..3ab4b44 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -35,6 +35,7 @@ def find_free_port(start=28021, end=28031): continue return 28011 # fallback default + def ensure_pine_settings(ini_path: str, port: int = 28011): """Ensure INI has configuration for PINE.""" config = ConfigParser() @@ -77,20 +78,22 @@ def ensure_pine_settings(ini_path: str, port: int = 28011): with open(ini_path, 'w') as f: config.write(f) + def setup_pine(): - """Determine port and create Pine instance""" + """Determine port and create Pine instance.""" host_settings = get_settings() game_ini = host_settings.get('rac2_options', {}).get('game_ini') + # Only pick port here; do not touch ini yet if game_ini and os.path.exists(os.path.dirname(game_ini)): port = find_free_port() - ensure_pine_settings(game_ini, port) else: port = 28011 create_pine_interface(port) return port + # Run early so Pine instance exists for Rac2Interface pine_port = setup_pine() @@ -319,6 +322,7 @@ async def run_game(iso_file): async def patch_and_run_game(aprac2_file: str): + """Patch ISO if needed, ensure copied INI has correct PINE configuration, and launch game.""" settings: Optional[Rac2Settings] = get_settings().get("rac2_options", False) assert settings, "No Rac2 Settings?" @@ -326,7 +330,7 @@ async def patch_and_run_game(aprac2_file: str): base_name = os.path.splitext(aprac2_file)[0] output_path = base_name + '.iso' - # --- Always determine CRC + version once the ISO exists or is created --- + # Patch ISO if missing if not os.path.exists(output_path): from .PatcherUI import PatcherUI patcher = PatcherUI(aprac2_file, output_path, logger) @@ -342,10 +346,13 @@ async def patch_and_run_game(aprac2_file: str): file_name = f"{version}_{crc:X}.ini" file_path = os.path.join(os.path.dirname(game_ini_path), file_name) - # Always ensure up-to-date settings (even if ISO already existed) + # Always create or refresh a CRC-based ini copy shutil.copy(game_ini_path, file_path) ensure_pine_settings(file_path, pine_port) - logger.info(f"Configured PINE on port {pine_port} in {os.path.basename(file_path)}") + + logger.info(f"Configured PINE (port {pine_port}) in {os.path.basename(file_path)}") + else: + logger.warning("No valid game_ini found; skipping INI setup.") Utils.async_start(run_game(output_path)) From ece2709b8c15b1e80b5f37947da2890cad3b5b67 Mon Sep 17 00:00:00 2001 From: jacobmix Date: Sat, 18 Oct 2025 02:18:45 +0200 Subject: [PATCH 08/13] Update Rac2Client.py with /start command Added /start command to launch .aprac2 Included checks to make sure host.yaml has propper settings. --- Rac2Client.py | 96 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index 3ab4b44..9a45631 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -8,6 +8,8 @@ import subprocess import traceback import socket +import tkinter as tk +from tkinter import filedialog from CommonClient import ClientCommandProcessor, CommonContext, get_base_parser, logger, server_loop, gui_enabled from NetUtils import ClientStatus @@ -35,7 +37,6 @@ def find_free_port(start=28021, end=28031): continue return 28011 # fallback default - def ensure_pine_settings(ini_path: str, port: int = 28011): """Ensure INI has configuration for PINE.""" config = ConfigParser() @@ -78,7 +79,6 @@ def ensure_pine_settings(ini_path: str, port: int = 28011): with open(ini_path, 'w') as f: config.write(f) - def setup_pine(): """Determine port and create Pine instance.""" host_settings = get_settings() @@ -93,10 +93,55 @@ def setup_pine(): create_pine_interface(port) return port - # Run early so Pine instance exists for Rac2Interface pine_port = setup_pine() +def validate_rac2_settings(): + """Validate rac2_options from host.yaml before continuing.""" + host_settings = get_settings() + rac2_opts = host_settings.get("rac2_options", {}) + + problems = [] + + # ISO file + iso_file = rac2_opts.get("iso_file") + if not iso_file: + problems.append("Missing 'iso_file' in rac2_options.") + else: + iso_file_expanded = os.path.expandvars(os.path.expanduser(iso_file)) + if not os.path.isfile(iso_file_expanded): + problems.append(f"ISO file not found: {iso_file_expanded}") + + # ISO start (PCSX2 path) + iso_start = rac2_opts.get("iso_start") + if iso_start in (None, ""): + problems.append("Missing 'iso_start' — should be path to PCSX2.") + elif isinstance(iso_start, str): + iso_start_expanded = os.path.expandvars(os.path.expanduser(iso_start)) + if not os.path.isfile(iso_start_expanded): + problems.append(f"'iso_start' path does not exist: {iso_start_expanded}") + elif not iso_start_expanded.lower().endswith(("pcsx2.exe", "pcsx2-qt.exe")): + problems.append(f"'iso_start' doesn't look like a PCSX2 executable (pcsx2.exe or pcsx2-qt.exe): {iso_start_expanded}") + + # Game INI + game_ini = rac2_opts.get("game_ini") + if not game_ini: + problems.append("Missing 'game_ini' path — should point to a PCSX2 game settings INI file.") + else: + game_ini_expanded = os.path.expandvars(os.path.expanduser(game_ini)) + if not os.path.isfile(game_ini_expanded): + problems.append(f"Game INI not found: {game_ini_expanded}") + + # Report + if problems: + logger.warning("Rac2 configuration issues detected:") + for p in problems: + logger.warning(f" - {p}") + logger.warning("Please check your host.yaml rac2_options section, and restart.") + return False + + # logger.info("rac2_options verified successfully.") + return True class Rac2CommandProcessor(ClientCommandProcessor): def __init__(self, ctx: CommonContext): @@ -134,6 +179,51 @@ def _cmd_deathlink(self): logger.info(message) self.ctx.notification_manager.queue_notification(message) + def _cmd_start(self): + """Select and start with a .aprac2 patch file.""" + if not isinstance(self.ctx, Rac2Context): + logger.error("Not in a valid RAC2 context.") + return + + # Prevent launching if already connected to PCSX2 + if self.ctx.game_interface.get_connection_state(): + msg = "Already connected to Ratchet & Clank 2 / PCSX2 — please close old instance or open another client." + logger.warning(msg) + self.ctx.notification_manager.queue_notification(msg) + return + + # Validate rac2 host.yaml options + if not validate_rac2_settings(): + return + + # Open file dialog + root = tk.Tk() + root.withdraw() + file_path = filedialog.askopenfilename( + title="Select a Ratchet & Clank 2 .aprac2 file", + filetypes=[("Archipelago RAC2 Patch Files", "*.aprac2"), ("All Files", "*.*")] + ) + root.destroy() + + if not file_path: + logger.info("No file selected.") + return + + # Launch async patching + game startup + logger.info(f"Selected patch: {file_path}") + self.ctx.notification_manager.queue_notification("Launching selected patch file...") + + async def start_patch(): + try: + await patch_and_run_game(file_path) + self.ctx.auth = get_name_from_aprac2(file_path) + logger.info("Game launch initiated.") + except Exception as e: + logger.error(f"Failed to start patch: {e}") + self.ctx.notification_manager.queue_notification(f"Error: {e}") + + Utils.async_start(start_patch(), name="Manual Patch Launch") + class Rac2Context(CommonContext): current_planet: Optional[Rac2Planet] = None From 02aebde5e10914182a7787a3dcb35378745330dc Mon Sep 17 00:00:00 2001 From: jacobmix Date: Sat, 18 Oct 2025 03:30:43 +0200 Subject: [PATCH 09/13] Update __init__.py to verify copied iso size Now verify iso copy when creating vanilla iso in archipelago directory. Should hopefully fix problems some users have. Could still happen if people close mid copy or ignore errors. But client should warn against that. --- __init__.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 8f656aa..c3bb171 100644 --- a/__init__.py +++ b/__init__.py @@ -33,6 +33,37 @@ class IsoFile(settings.UserFilePath): description = "Ratchet & Clank 2 PS2 ISO file" copy_to = "Ratchet & Clank 2.iso" + def copy(self, target_dir: str): + """Copy ISO and confirm the copy is complete.""" + from CommonClient import logger # safely import logger here to avoid circular imports + + src = os.path.expanduser(os.path.expandvars(self.value)) + if not os.path.isfile(src): + raise FileNotFoundError(f"Ratchet & Clank 2 ISO not found: {src}") + + dst = os.path.join(target_dir, os.path.basename(self.copy_to)) + + logger.info("Copying ISO to Archipelago directory — please don't close the client...") + logger.info(f"Source: {os.path.basename(src)} — Destination: {dst}") + + # Perform copy + result = super().copy(target_dir) + + # Verify sizes match exactly + src_size = os.path.getsize(src) + dst_size = os.path.getsize(dst) + + if src_size != dst_size: + raise IOError( + f"ISO copy verification failed.\n" + f"Source: {src_size:,} bytes\n" + f"Copied: {dst_size:,} bytes\n" + f"({dst} may be incomplete or corrupted)" + ) + + logger.info("ISO copy completed successfully.") + return result + class IsoStart(str): """ Set this false to never autostart an iso (such as after patching), @@ -147,4 +178,4 @@ def get_options_as_dict(self) -> Dict[str, Any]: ) def fill_slot_data(self) -> Mapping[str, Any]: - return self.get_options_as_dict() \ No newline at end of file + return self.get_options_as_dict() From 286b5cddad88e7498fb81457f3ff5f7c7c6d5665 Mon Sep 17 00:00:00 2001 From: jacobmix Date: Sun, 19 Oct 2025 01:04:33 +0200 Subject: [PATCH 10/13] Update __init__.py Logging doesn't work. Main issue is probably people closing the client anyway. Could put a message in to tell people to not close the client. But already get an error when trying to open the patch message after copying iso. --- __init__.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/__init__.py b/__init__.py index c3bb171..2161bb2 100644 --- a/__init__.py +++ b/__init__.py @@ -33,37 +33,6 @@ class IsoFile(settings.UserFilePath): description = "Ratchet & Clank 2 PS2 ISO file" copy_to = "Ratchet & Clank 2.iso" - def copy(self, target_dir: str): - """Copy ISO and confirm the copy is complete.""" - from CommonClient import logger # safely import logger here to avoid circular imports - - src = os.path.expanduser(os.path.expandvars(self.value)) - if not os.path.isfile(src): - raise FileNotFoundError(f"Ratchet & Clank 2 ISO not found: {src}") - - dst = os.path.join(target_dir, os.path.basename(self.copy_to)) - - logger.info("Copying ISO to Archipelago directory — please don't close the client...") - logger.info(f"Source: {os.path.basename(src)} — Destination: {dst}") - - # Perform copy - result = super().copy(target_dir) - - # Verify sizes match exactly - src_size = os.path.getsize(src) - dst_size = os.path.getsize(dst) - - if src_size != dst_size: - raise IOError( - f"ISO copy verification failed.\n" - f"Source: {src_size:,} bytes\n" - f"Copied: {dst_size:,} bytes\n" - f"({dst} may be incomplete or corrupted)" - ) - - logger.info("ISO copy completed successfully.") - return result - class IsoStart(str): """ Set this false to never autostart an iso (such as after patching), From 308ca336e2dec124eb8a0f0db489e51d8b74c94b Mon Sep 17 00:00:00 2001 From: jacobmix Date: Wed, 22 Oct 2025 14:55:25 +0200 Subject: [PATCH 11/13] Start command now only warns & works with other OS --- Rac2Client.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index 9a45631..de6279e 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -8,6 +8,7 @@ import subprocess import traceback import socket +import platform import tkinter as tk from tkinter import filedialog @@ -96,14 +97,15 @@ def setup_pine(): # Run early so Pine instance exists for Rac2Interface pine_port = setup_pine() -def validate_rac2_settings(): - """Validate rac2_options from host.yaml before continuing.""" +def validate_rac2_settings() -> bool: + """Validate rac2_options from host.yaml before continuing. + Logs warnings but does not abort.""" host_settings = get_settings() rac2_opts = host_settings.get("rac2_options", {}) problems = [] - # ISO file + # ISO file check iso_file = rac2_opts.get("iso_file") if not iso_file: problems.append("Missing 'iso_file' in rac2_options.") @@ -114,14 +116,21 @@ def validate_rac2_settings(): # ISO start (PCSX2 path) iso_start = rac2_opts.get("iso_start") - if iso_start in (None, ""): - problems.append("Missing 'iso_start' — should be path to PCSX2.") + if not iso_start: + problems.append("Missing 'iso_start' — should be path to PCSX2 executable.") elif isinstance(iso_start, str): iso_start_expanded = os.path.expandvars(os.path.expanduser(iso_start)) if not os.path.isfile(iso_start_expanded): problems.append(f"'iso_start' path does not exist: {iso_start_expanded}") - elif not iso_start_expanded.lower().endswith(("pcsx2.exe", "pcsx2-qt.exe")): - problems.append(f"'iso_start' doesn't look like a PCSX2 executable (pcsx2.exe or pcsx2-qt.exe): {iso_start_expanded}") + else: + system = platform.system().lower() + exe_name = os.path.basename(iso_start_expanded).lower() + # Windows check + if system == "windows" and not exe_name.endswith(("pcsx2.exe", "pcsx2-qt.exe")): + problems.append(f"On Windows, PCSX2 executable usually ends with pcsx2.exe — got {exe_name}") + # Linux/macOS check + elif system in ("linux", "darwin") and "pcsx2" not in exe_name: + problems.append(f"Expected 'pcsx2' binary on {system.capitalize()}, got {exe_name}") # Game INI game_ini = rac2_opts.get("game_ini") @@ -132,16 +141,22 @@ def validate_rac2_settings(): if not os.path.isfile(game_ini_expanded): problems.append(f"Game INI not found: {game_ini_expanded}") - # Report + # Always warn, never block if problems: - logger.warning("Rac2 configuration issues detected:") + logger.warning("⚠ Rac2 configuration issues detected:") for p in problems: logger.warning(f" - {p}") - logger.warning("Please check your host.yaml rac2_options section, and restart.") - return False + logger.warning("Continuing anyway; the game may still launch normally.") + + # Notify user in-client + try: + ctx = globals().get("ctx") # use context if available + if ctx and hasattr(ctx, "notification_manager"): + ctx.notification_manager.queue_notification("Some RAC2 config issues found (see above). Continuing anyway.") + except Exception: + pass - # logger.info("rac2_options verified successfully.") - return True + return True # Always return True now class Rac2CommandProcessor(ClientCommandProcessor): def __init__(self, ctx: CommonContext): From c650a5dc36c1259a88b5cc879934c454e1dc6a2c Mon Sep 17 00:00:00 2001 From: jacobmix Date: Wed, 22 Oct 2025 15:15:35 +0200 Subject: [PATCH 12/13] PINE Linux socket update --- pcsx2_interface/pine.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/pcsx2_interface/pine.py b/pcsx2_interface/pine.py index bcbe967..b8ad651 100644 --- a/pcsx2_interface/pine.py +++ b/pcsx2_interface/pine.py @@ -71,30 +71,33 @@ def __init__(self, slot: int = 28011): # self._init_socket() def _init_socket(self) -> None: - if system() == "Windows": - socket_family = socket.AF_INET - socket_name = ("127.0.0.1", self._slot) - elif system() == "Linux": - socket_family = socket.AF_UNIX - socket_name = os.environ.get("XDG_RUNTIME_DIR", "/tmp") - socket_name += "/pcsx2.sock" - elif system() == "Darwin": - socket_family = socket.AF_UNIX - socket_name = os.environ.get("TMPDIR", "/tmp") - socket_name += "/pcsx2.sock" - else: - socket_family = socket.AF_UNIX - socket_name = "/tmp/pcsx2.sock" - + system_name = system() try: + if system_name == "Windows": + socket_family = socket.AF_INET + socket_name = ("127.0.0.1", self._slot) + elif system_name in ("Linux", "Darwin"): + socket_family = socket.AF_UNIX + # Use XDG_RUNTIME_DIR or fallback to /tmp + base_dir = os.environ.get("XDG_RUNTIME_DIR") or os.environ.get("TMPDIR") or "/tmp" + # PCSX2 uses `.sock` for 28011, otherwise `.sock.` + if self._slot == 28011: + socket_name = os.path.join(base_dir, "pcsx2.sock") + else: + socket_name = os.path.join(base_dir, f"pcsx2.sock.{self._slot}") + else: + # Fallback for unknown UNIX-like systems + socket_family = socket.AF_UNIX + socket_name = f"/tmp/pcsx2.sock.{self._slot}" if self._slot != 28011 else "/tmp/pcsx2.sock" + self._sock = socket.socket(socket_family, socket.SOCK_STREAM) self._sock.settimeout(5.0) self._sock.connect(socket_name) - except socket.error: + except socket.error as e: self._sock.close() self._sock_state = False return - + self._sock_state = True def connect(self) -> None: From bd076e08b985ed815fffad8e689cd2fa45bf8511 Mon Sep 17 00:00:00 2001 From: jacobmix Date: Wed, 22 Oct 2025 16:30:01 +0200 Subject: [PATCH 13/13] Skip busy ports properly for Linux --- Rac2Client.py | 49 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/Rac2Client.py b/Rac2Client.py index de6279e..5a1d422 100644 --- a/Rac2Client.py +++ b/Rac2Client.py @@ -9,6 +9,7 @@ import traceback import socket import platform +import errno import tkinter as tk from tkinter import filedialog @@ -26,17 +27,53 @@ from .Rac2Interface import HUD_MESSAGE_DURATION, ConnectionState, create_pine_interface, Rac2Interface, Rac2Planet from configparser import ConfigParser +DEFAULT_PINE_PORT = 28011 def find_free_port(start=28021, end=28031): - """Find free port for PINE""" + system_name = platform.system() + + # On Windows, keep the old TCP-based logic + if system_name == "Windows": + for port in range(start, end + 1): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(("127.0.0.1", port)) + return port + except OSError: + continue + return DEFAULT_PINE_PORT + + # On Linux/macOS, check for existing socket files instead + base_dir = os.environ.get("XDG_RUNTIME_DIR") or os.environ.get("TMPDIR") or "/tmp" + for port in range(start, end + 1): - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + if port == DEFAULT_PINE_PORT: + sock_path = os.path.join(base_dir, "pcsx2.sock") + else: + sock_path = os.path.join(base_dir, f"pcsx2.sock.{port}") + + # If socket file exists, test whether it’s actually active + if os.path.exists(sock_path): try: - s.bind(("127.0.0.1", port)) + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: + s.connect(sock_path) + # If we connected, it’s in use + continue + except OSError as e: + # Connection failed → likely stale socket file, safe to remove + if e.errno in (errno.ECONNREFUSED, errno.ENOENT): + try: + os.remove(sock_path) + except OSError: + pass + # In either case, we can reuse this port now return port - except OSError: - continue - return 28011 # fallback default + else: + # No such file → definitely free + return port + + # Fallback if all taken + return DEFAULT_PINE_PORT def ensure_pine_settings(ini_path: str, port: int = 28011): """Ensure INI has configuration for PINE."""