prep for linux compatibility

This commit is contained in:
edschuy95 2026-01-20 15:57:22 -05:00
parent 66d5032085
commit 6cceb32037
3 changed files with 137 additions and 31 deletions

39
Quake3Arena.spec Normal file
View file

@ -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'],
)

View file

@ -6,14 +6,15 @@ import sys
import random import random
import re import re
import queue import queue
#import readchar import tempfile
import ctypes import psutil
import subprocess import subprocess
import getpass
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1" os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
import pygame import pygame
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) #kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
# Event to signal shutdown # Event to signal shutdown
shutdown_event = threading.Event() shutdown_event = threading.Event()
@ -21,19 +22,29 @@ shutdown_event = threading.Event()
ERROR_ALREADY_EXISTS = 183 ERROR_ALREADY_EXISTS = 183
# ========================= CONFIG ========================= # ========================= 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 paths (used if not in debug mode)
DEFAULT_GAME_EXE = r".\qgame.dll" DEFAULT_WIN_GAME_EXE = r"./qgame.dll"
DEFAULT_PLAYLIST = r".\arena\music\playlist.txt" DEFAULT_LIN_GAME_EXE = r"./qgame.so"
DEFAULT_RA3_MAPS = r".\arena\music\ra3_maps.txt" DEFAULT_PLAYLIST = r"./arena/music/playlist.txt"
DEFAULT_ARENA0_BL = r".\arena\music\arena0_bl.txt" DEFAULT_RA3_MAPS = r"./arena/music/ra3_maps.txt"
DEFAULT_ARENA0_BL = r"./arena/music/arena0_bl.txt"
# Debug override paths # Debug override paths
DEBUG_GAME_EXE = r"D:\GOG Games\Quake III\qgame.dll" DEBUG_WIN_GAME_EXE = r"D:/GOG Games/Quake III/qgame.dll"
DEBUG_PLAYLIST = r"D:\GOG Games\Quake III\arena\music\playlist.txt" DEBUG_LIN_GAME_EXE = r"D:/GOG Games/Quake III/qgame.so"
DEBUG_RA3_MAPS = r"D:\GOG Games\Quake III\arena\music\ra3_maps.txt" DEBUG_PLAYLIST = r"D:/GOG Games/Quake III/arena/music/playlist.txt"
DEBUG_ARENA0_BL = r"D:\GOG Games\Quake III\arena\music\arena0_bl.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 # Initial volume
VOLUME_STEP = 0.1 # step for W/S volume control VOLUME_STEP = 0.1 # step for W/S volume control
@ -78,6 +89,54 @@ ANSI_ESCAPE_RE = re.compile(
# ========================================================== # ==========================================================
# ===================== UTILITY FUNCTIONS ================= # ===================== 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: def Check_Arena_Blacklist(arenaline: str) -> bool:
global arena0_bl_path global arena0_bl_path
arena_bl_local = load_arena0_blacklist(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 True
return False return False
def acquire_single_instance(name: str): # def acquire_single_instance(name: str):
handle = kernel32.CreateMutexW( # handle = kernel32.CreateMutexW(
None, # lpMutexAttributes # None, # lpMutexAttributes
False, # bInitialOwner # False, # bInitialOwner
name # lpName # name # lpName
) # )
if not handle: # if not handle:
raise ctypes.WinError(ctypes.get_last_error()) # raise ctypes.WinError(ctypes.get_last_error())
if kernel32.GetLastError() == ERROR_ALREADY_EXISTS: # if kernel32.GetLastError() == ERROR_ALREADY_EXISTS:
kernel32.CloseHandle(handle) # kernel32.CloseHandle(handle)
return None # return None
return handle # return handle
def strip_ansi(text: str) -> str: def strip_ansi(text: str) -> str:
return ANSI_ESCAPE_RE.sub("", text) return ANSI_ESCAPE_RE.sub("", text)
@ -465,11 +524,11 @@ def send_command(proc, cmd):
# ======================== MAIN =========================== # ======================== MAIN ===========================
def main(): def main():
mutex = acquire_single_instance("Global\\q3aLauncherAndMp3Playback") # mutex = acquire_single_instance("Global\\q3aLauncherAndMp3Playback")
if mutex is None: # if mutex is None:
print("Another instance is already running.") # print("Another instance is already running.")
sys.exit(1) # sys.exit(1)
global playlist, ra3_maps, pty_proc, playlist_path, ra3_maps_path, arena0_blacklist, arena0_bl_path 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 # # Launch quake process via subprocess
pty_proc = subprocess.Popen( pty_proc = subprocess.Popen(
[game_exe, "--showterminalconsole"] + sys.argv[1:], [game_exe, "+set", "ttycon", "1"] + sys.argv[1:],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
@ -542,5 +601,13 @@ def main():
pygame.mixer.quit() pygame.mixer.quit()
if __name__ == "__main__": 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)
# ========================================================== # ==========================================================

View file

@ -1 +1 @@
pyinstaller --onefile --name RA3WITHMP3 --icon=quake3modern.ico --collect-all readchar RA3MP3Playback.py pyinstaller --onefile --name Quake3Arena --icon=quake3modern.ico --exclude-module tkinter --exclude-module unittest --exclude-module http --exclude-module pydoc --exclude-module doctest RA3MP3Playback.py