prep for linux compatibility
This commit is contained in:
parent
66d5032085
commit
6cceb32037
3 changed files with 137 additions and 31 deletions
39
Quake3Arena.spec
Normal file
39
Quake3Arena.spec
Normal 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'],
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue