From 6cceb320371b02230d72b467f2e90f67b96f7530 Mon Sep 17 00:00:00 2001 From: edschuy95 Date: Tue, 20 Jan 2026 15:57:22 -0500 Subject: [PATCH] prep for linux compatibility --- Quake3Arena.spec | 39 ++++++++++++++ RA3MP3Playback.py | 127 +++++++++++++++++++++++++++++++++++----------- build.bat | 2 +- 3 files changed, 137 insertions(+), 31 deletions(-) create mode 100644 Quake3Arena.spec diff --git a/Quake3Arena.spec b/Quake3Arena.spec new file mode 100644 index 0000000..3266df8 --- /dev/null +++ b/Quake3Arena.spec @@ -0,0 +1,39 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['RA3MP3Playback.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=['tkinter', 'unittest', 'http', 'pydoc', 'doctest'], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='Quake3Arena', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['quake3modern.ico'], +) diff --git a/RA3MP3Playback.py b/RA3MP3Playback.py index a27b923..2b8bafa 100644 --- a/RA3MP3Playback.py +++ b/RA3MP3Playback.py @@ -6,14 +6,15 @@ import sys import random import re import queue -#import readchar -import ctypes +import tempfile +import psutil import subprocess +import getpass os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1" import pygame -kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) +#kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # Event to signal shutdown shutdown_event = threading.Event() @@ -21,19 +22,29 @@ shutdown_event = threading.Event() ERROR_ALREADY_EXISTS = 183 # ========================= CONFIG ========================= -DEBUG = False # Set True to enable debug overrides +DEBUG = True # Set True to enable debug overrides # Default paths (used if not in debug mode) -DEFAULT_GAME_EXE = r".\qgame.dll" -DEFAULT_PLAYLIST = r".\arena\music\playlist.txt" -DEFAULT_RA3_MAPS = r".\arena\music\ra3_maps.txt" -DEFAULT_ARENA0_BL = r".\arena\music\arena0_bl.txt" +DEFAULT_WIN_GAME_EXE = r"./qgame.dll" +DEFAULT_LIN_GAME_EXE = r"./qgame.so" +DEFAULT_PLAYLIST = r"./arena/music/playlist.txt" +DEFAULT_RA3_MAPS = r"./arena/music/ra3_maps.txt" +DEFAULT_ARENA0_BL = r"./arena/music/arena0_bl.txt" # Debug override paths -DEBUG_GAME_EXE = r"D:\GOG Games\Quake III\qgame.dll" -DEBUG_PLAYLIST = r"D:\GOG Games\Quake III\arena\music\playlist.txt" -DEBUG_RA3_MAPS = r"D:\GOG Games\Quake III\arena\music\ra3_maps.txt" -DEBUG_ARENA0_BL = r"D:\GOG Games\Quake III\arena\music\arena0_bl.txt" +DEBUG_WIN_GAME_EXE = r"D:/GOG Games/Quake III/qgame.dll" +DEBUG_LIN_GAME_EXE = r"D:/GOG Games/Quake III/qgame.so" +DEBUG_PLAYLIST = r"D:/GOG Games/Quake III/arena/music/playlist.txt" +DEBUG_RA3_MAPS = r"D:/GOG Games/Quake III/arena/music/ra3_maps.txt" +DEBUG_ARENA0_BL = r"D:/GOG Games/Quake III/arena/music/arena0_bl.txt" + +if (os.name == "nt"): + DEFAULT_GAME_EXE = DEFAULT_WIN_GAME_EXE + DEBUG_GAME_EXE = DEBUG_WIN_GAME_EXE +else: + DEFAULT_GAME_EXE = DEFAULT_LIN_GAME_EXE + DEBUG_GAME_EXE = DEBUG_LIN_GAME_EXE + # Initial volume VOLUME_STEP = 0.1 # step for W/S volume control @@ -78,6 +89,54 @@ ANSI_ESCAPE_RE = re.compile( # ========================================================== # ===================== UTILITY FUNCTIONS ================= +def acquire_single_instance(lockfile_base: str): + """ + Ensures only one instance of the program is running per user. + Uses a lock file in the temp directory and psutil to check for stale locks. + Returns (file descriptor, lockfile path) if acquired, None if another instance is running. + """ + username = getpass.getuser() + lockfile_name = f"{lockfile_base}_{username}.lock" + lockfile_path = os.path.join(tempfile.gettempdir(), lockfile_name) + + # If lock file exists, check if PID inside is running + if os.path.exists(lockfile_path): + try: + with open(lockfile_path, "r") as f: + pid = int(f.read()) + if psutil.pid_exists(pid): + # Another instance is running + return None, lockfile_path + else: + # Stale lock file; remove it + os.unlink(lockfile_path) + except Exception: + # Could not read PID; remove stale lock + try: + os.unlink(lockfile_path) + except: + pass + + # Create lock file exclusively + try: + fd = os.open(lockfile_path, os.O_CREAT | os.O_EXCL | os.O_RDWR) + os.write(fd, str(os.getpid()).encode()) + return fd, lockfile_path + except FileExistsError: + # Race condition: another process created it just now + return None, lockfile_path + +def release_single_instance(fd, lockfile_path): + """Closes the lock file and removes it.""" + try: + os.close(fd) + except: + pass + try: + os.unlink(lockfile_path) + except: + pass + def Check_Arena_Blacklist(arenaline: str) -> bool: global arena0_bl_path arena_bl_local = load_arena0_blacklist(arena0_bl_path) @@ -86,21 +145,21 @@ def Check_Arena_Blacklist(arenaline: str) -> bool: return True return False -def acquire_single_instance(name: str): - handle = kernel32.CreateMutexW( - None, # lpMutexAttributes - False, # bInitialOwner - name # lpName - ) +# def acquire_single_instance(name: str): +# handle = kernel32.CreateMutexW( +# None, # lpMutexAttributes +# False, # bInitialOwner +# name # lpName +# ) - if not handle: - raise ctypes.WinError(ctypes.get_last_error()) +# if not handle: +# raise ctypes.WinError(ctypes.get_last_error()) - if kernel32.GetLastError() == ERROR_ALREADY_EXISTS: - kernel32.CloseHandle(handle) - return None +# if kernel32.GetLastError() == ERROR_ALREADY_EXISTS: +# kernel32.CloseHandle(handle) +# return None - return handle +# return handle def strip_ansi(text: str) -> str: return ANSI_ESCAPE_RE.sub("", text) @@ -465,11 +524,11 @@ def send_command(proc, cmd): # ======================== MAIN =========================== def main(): - mutex = acquire_single_instance("Global\\q3aLauncherAndMp3Playback") + # mutex = acquire_single_instance("Global\\q3aLauncherAndMp3Playback") - if mutex is None: - print("Another instance is already running.") - sys.exit(1) + # if mutex is None: + # print("Another instance is already running.") + # sys.exit(1) global playlist, ra3_maps, pty_proc, playlist_path, ra3_maps_path, arena0_blacklist, arena0_bl_path @@ -503,7 +562,7 @@ def main(): # # Launch quake process via subprocess pty_proc = subprocess.Popen( - [game_exe, "--showterminalconsole"] + sys.argv[1:], + [game_exe, "+set", "ttycon", "1"] + sys.argv[1:], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -542,5 +601,13 @@ def main(): pygame.mixer.quit() if __name__ == "__main__": - main() + lock_fd, lock_path = acquire_single_instance("q3a_launcher.lock") + if lock_fd is None: + print("Another instance is already running.") + sys.exit(1) + try: + main() + finally: + release_single_instance(lock_fd, lock_path) + # ========================================================== diff --git a/build.bat b/build.bat index 4e42589..b0480b4 100644 --- a/build.bat +++ b/build.bat @@ -1 +1 @@ -pyinstaller --onefile --name RA3WITHMP3 --icon=quake3modern.ico --collect-all readchar RA3MP3Playback.py \ No newline at end of file +pyinstaller --onefile --name Quake3Arena --icon=quake3modern.ico --exclude-module tkinter --exclude-module unittest --exclude-module http --exclude-module pydoc --exclude-module doctest RA3MP3Playback.py \ No newline at end of file