add support for XLM pre-launch script integration and non-interactive configuration modes
This commit is contained in:
parent
72c301be78
commit
6f7d8c9d7c
4 changed files with 188 additions and 66 deletions
36
README.md
36
README.md
|
|
@ -10,6 +10,8 @@ This is a wrapper script for Linux that launches `xivlauncher-core` (Native or F
|
|||
- **URL Support**: Accepts standard Base32 secrets OR full `otpauth://` URLs (e.g. from QR codes).
|
||||
- **Auto-Configuration**: Prompts to fix `launcher.ini` settings if they are incorrect.
|
||||
- **Performance Optimization**: Automatically detects and integrates with `gamemoderun` or `game-performance` to optimize your game instance.
|
||||
- **XLM Pre-Launch Support**: Seamlessly integrates as a pre-launch script for the Steam Compatibility Tool XLM.
|
||||
- **Steam %command% Wrapper**: Can wrap arbitrary launch arguments natively in Steam launch options.
|
||||
|
||||
## Installation (Steam Deck / Linux Desktop)
|
||||
|
||||
|
|
@ -27,13 +29,31 @@ The easiest way to install is using the provided installer script. This script w
|
|||
* **Creates Config:** Creates `~/.config/xivlauncher-wrapper/config.json`.
|
||||
* **Installs Wrapper:** Installs the wrapper to `~/.local/bin/xivlauncher-wrapper`.
|
||||
* **Installs Shortcut:** Installs a `.desktop` file to `~/.local/share/applications/` so it appears in your application menu.
|
||||
* **XLM Setup:** Asks to install itself as an XLM pre-launch script by default.
|
||||
|
||||
### Steam Integration (Steam Deck)
|
||||
1. Switch to Desktop Mode.
|
||||
2. Open Steam.
|
||||
3. "Games" -> "Add a Non-Steam Game to My Library".
|
||||
4. Select "XIVLauncher Wrapper" from the list.
|
||||
5. Return to Gaming Mode and launch "XIVLauncher Wrapper".
|
||||
### Steam Integration Methods
|
||||
|
||||
You can use the wrapper with Steam in three different ways:
|
||||
|
||||
#### 1. XLM Pre-Launch Script (Recommended for Steam Deck)
|
||||
If you use XLM (XIVLauncher Manager) as a Steam Compatibility Tool for FFXIV, the installer can automatically set up the wrapper to run right before the game launches.
|
||||
If you skip this during installation, you can manually symlink the wrapper into your XLM directory:
|
||||
```bash
|
||||
mkdir -p ~/.local/share/Steam/compatibilitytools.d/XLCore/prelaunch.d
|
||||
ln -sf ~/.local/bin/xivlauncher-wrapper ~/.local/share/Steam/compatibilitytools.d/XLCore/prelaunch.d/xivlauncher-wrapper
|
||||
```
|
||||
*(The script automatically detects it is in a `prelaunch.d` directory and runs in `--prelaunch` background mode).*
|
||||
|
||||
#### 2. Steam Launch Options Wrap
|
||||
If you want to use it directly in the Steam properties of FFXIV without XLM, you can set your launch options to:
|
||||
```
|
||||
xivlauncher-wrapper %command%
|
||||
```
|
||||
|
||||
#### 3. Standalone Non-Steam Game
|
||||
1. Switch to Desktop Mode and open Steam.
|
||||
2. "Games" -> "Add a Non-Steam Game to My Library".
|
||||
3. Select "XIVLauncher Wrapper" from the list.
|
||||
|
||||
## Manual Setup
|
||||
|
||||
|
|
@ -64,6 +84,8 @@ Simply run the wrapper:
|
|||
./wrapper.py
|
||||
```
|
||||
|
||||
Or launch "XIVLauncher Wrapper" from your application menu/Steam.
|
||||
**Advanced CLI options:**
|
||||
- `--prelaunch`: Spawns the OTP injector in the background and exits immediately. Useful for custom wrapper scripts.
|
||||
- Wrapping a specific command: `./wrapper.py flatpak run dev.goats.xivlauncher`
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
{
|
||||
"launcher_cmd": "flatpak run dev.goats.xivlauncher",
|
||||
"launcher_cmd": "",
|
||||
"secret": "",
|
||||
"use_gamemode": false,
|
||||
"gamemode_cmd": "gamemoderun"
|
||||
"gamemode_cmd": "gamemoderun",
|
||||
"_notes": {
|
||||
"launcher_cmd": "Leave empty for auto-detection. Can also be passed via CLI args (e.g., Steam %command%)",
|
||||
"secret": "Leave empty to use system keyring or prompt on launch",
|
||||
"use_gamemode": "Set to true to use a performance tool like gamemoderun or game-performance",
|
||||
"prelaunch": "The wrapper supports running automatically as an XLM prelaunch script if placed in prelaunch.d/"
|
||||
}
|
||||
}
|
||||
45
installer.sh
45
installer.sh
|
|
@ -191,6 +191,51 @@ else
|
|||
echo "update-desktop-database not found, skipping."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Checking for XLM compatibility tool..."
|
||||
XLM_PATHS=(
|
||||
"$HOME/.local/share/Steam/compatibilitytools.d/XLCore"
|
||||
"$HOME/.local/share/Steam/compatibilitytools.d/XLM"
|
||||
"$HOME/.local/share/Steam/compatibilitytools.d/xlm"
|
||||
"$HOME/.steam/root/compatibilitytools.d/XLCore"
|
||||
"$HOME/.steam/root/compatibilitytools.d/XLM"
|
||||
"$HOME/.steam/root/compatibilitytools.d/xlm"
|
||||
)
|
||||
|
||||
XLM_FOUND=""
|
||||
for p in "${XLM_PATHS[@]}"; do
|
||||
if [ -d "$p" ]; then
|
||||
XLM_FOUND="$p"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Would you like to install the wrapper as a pre-launch script for XLM?"
|
||||
echo "This allows the OTP injector to run automatically when launching FFXIV via Steam with XLM."
|
||||
read -p "Install as XLM pre-launch script? [Y/n]: " INSTALL_PRELAUNCH
|
||||
if [[ ! "$INSTALL_PRELAUNCH" =~ ^[Nn]$ ]]; then
|
||||
if [ -n "$XLM_FOUND" ]; then
|
||||
echo "Found XLM at $XLM_FOUND"
|
||||
mkdir -p "$XLM_FOUND/prelaunch.d"
|
||||
ln -sf "$BIN_DIR/xivlauncher-wrapper" "$XLM_FOUND/prelaunch.d/xivlauncher-wrapper"
|
||||
echo "Successfully linked wrapper to $XLM_FOUND/prelaunch.d/"
|
||||
else
|
||||
echo "Could not automatically detect XLM installation path."
|
||||
echo "If you have XLM installed, please enter the path to your XLM compatibility tool folder."
|
||||
echo "(Leave blank to skip)"
|
||||
read -p "> " CUSTOM_XLM_PATH
|
||||
if [ -n "$CUSTOM_XLM_PATH" ] && [ -d "$CUSTOM_XLM_PATH" ]; then
|
||||
mkdir -p "$CUSTOM_XLM_PATH/prelaunch.d"
|
||||
ln -sf "$BIN_DIR/xivlauncher-wrapper" "$CUSTOM_XLM_PATH/prelaunch.d/xivlauncher-wrapper"
|
||||
echo "Successfully linked wrapper to $CUSTOM_XLM_PATH/prelaunch.d/"
|
||||
else
|
||||
echo "Skipping XLM pre-launch script installation."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Skipping XLM pre-launch script setup."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Installation complete!"
|
||||
echo "You may need to add $BIN_DIR to your PATH if it's not already there."
|
||||
|
|
|
|||
155
wrapper.py
155
wrapper.py
|
|
@ -13,6 +13,7 @@ import hashlib
|
|||
import os
|
||||
import shutil
|
||||
import configparser
|
||||
import argparse
|
||||
try:
|
||||
import keyring
|
||||
HAS_KEYRING = True
|
||||
|
|
@ -142,7 +143,11 @@ def check_and_update_launcher_ini():
|
|||
print(f"\nConfiguration Check: '{ini_path}' not found.")
|
||||
print("Please open XIVLauncher, go to Settings, and enable 'Start internal OTP server'.")
|
||||
print("This is required for the wrapper to function.")
|
||||
input("Press Enter to continue once you have done this (or to try anyway)...")
|
||||
if sys.stdin.isatty():
|
||||
try:
|
||||
input("Press Enter to continue once you have done this (or to try anyway)...")
|
||||
except EOFError:
|
||||
pass
|
||||
return
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
|
|
@ -164,7 +169,16 @@ def check_and_update_launcher_ini():
|
|||
print(f"\nConfiguration Check: '{ini_path}'")
|
||||
print("XIVLauncher requires 'IsOtpServer' to be enabled to accept OTP codes from this wrapper.")
|
||||
|
||||
choice = input("Enable 'IsOtpServer' now? [Y/n]: ").strip().lower()
|
||||
choice = 'n'
|
||||
if sys.stdin.isatty():
|
||||
try:
|
||||
choice = input("Enable 'IsOtpServer' now? [Y/n]: ").strip().lower()
|
||||
except EOFError:
|
||||
choice = 'y'
|
||||
else:
|
||||
print("Non-interactive mode detected. Auto-enabling 'IsOtpServer'.")
|
||||
choice = 'y'
|
||||
|
||||
if choice in ('', 'y', 'yes'):
|
||||
config.set('Main', 'IsOtpServer', 'true')
|
||||
try:
|
||||
|
|
@ -257,6 +271,10 @@ def get_secret(config, config_path):
|
|||
|
||||
if not secret_input:
|
||||
print("GUI prompt unavailable or cancelled. Falling back to CLI.")
|
||||
if not sys.stdin.isatty():
|
||||
print("Error: No interactive terminal available and GUI prompt failed. Cannot prompt for secret.")
|
||||
return None
|
||||
|
||||
print("Please enter your TOTP Secret (base32) or otpauth:// URL.")
|
||||
while True:
|
||||
try:
|
||||
|
|
@ -268,6 +286,9 @@ def get_secret(config, config_path):
|
|||
except KeyboardInterrupt:
|
||||
print("\nOperation cancelled.")
|
||||
sys.exit(1)
|
||||
except EOFError:
|
||||
print("\nError: EOF reading secret.")
|
||||
return None
|
||||
|
||||
# Basic validation/parsing check
|
||||
parsed = parse_secret(secret_input)
|
||||
|
|
@ -298,6 +319,16 @@ def get_secret(config, config_path):
|
|||
return parsed
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="XIVLauncher Wrapper")
|
||||
parser.add_argument('--prelaunch', action='store_true', help="Run as a prelaunch script (spawns injector and exits)")
|
||||
parser.add_argument('--inject-only', action='store_true', help=argparse.SUPPRESS)
|
||||
args, unknown_args = parser.parse_known_args()
|
||||
|
||||
# Auto-detect if running from a prelaunch.d directory
|
||||
script_path = os.path.abspath(sys.argv[0])
|
||||
if 'prelaunch.d' in script_path.split(os.sep):
|
||||
args.prelaunch = True
|
||||
|
||||
# Determine config path (XDG standard)
|
||||
xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
||||
config_dir = os.path.join(xdg_config_home, 'xivlauncher-wrapper')
|
||||
|
|
@ -332,52 +363,62 @@ def main():
|
|||
check_and_update_launcher_ini()
|
||||
|
||||
cmd = config.get("launcher_cmd")
|
||||
|
||||
config_dirty = False
|
||||
cmd_args = None
|
||||
|
||||
# 2. Auto-detect command if missing
|
||||
if not cmd:
|
||||
print("\n'launcher_cmd' not set in config. detecting installed XIVLauncher versions...")
|
||||
available = detect_launch_command()
|
||||
# Determine command to launch (if not prelaunch/inject-only)
|
||||
if not args.prelaunch and not args.inject_only:
|
||||
if unknown_args:
|
||||
# Command passed via args (Steam %command% mode)
|
||||
cmd_args = unknown_args
|
||||
cmd = " ".join(shlex.quote(a) for a in cmd_args)
|
||||
else:
|
||||
# 2. Auto-detect command if missing
|
||||
if not cmd:
|
||||
print("\n'launcher_cmd' not set in config. detecting installed XIVLauncher versions...")
|
||||
available = detect_launch_command()
|
||||
|
||||
if not available:
|
||||
print("Error: No XIVLauncher installation found (Flatpak or Native).")
|
||||
print("Please install XIVLauncher or manually set 'launcher_cmd' in config.json.")
|
||||
sys.exit(1)
|
||||
if not available:
|
||||
print("Error: No XIVLauncher installation found (Flatpak or Native).")
|
||||
print("Please install XIVLauncher or manually set 'launcher_cmd' in config.json.")
|
||||
sys.exit(1)
|
||||
|
||||
elif len(available) >= 1:
|
||||
name, command = available[0]
|
||||
if len(available) > 1:
|
||||
print(f"Multiple XIVLauncher installations found. Automatically selecting first available: {name}")
|
||||
else:
|
||||
print(f"Detected {name}: {command}")
|
||||
cmd = command
|
||||
config['launcher_cmd'] = cmd
|
||||
config_dirty = True
|
||||
elif len(available) >= 1:
|
||||
name, command = available[0]
|
||||
if len(available) > 1:
|
||||
print(f"Multiple XIVLauncher installations found. Automatically selecting first available: {name}")
|
||||
else:
|
||||
print(f"Detected {name}: {command}")
|
||||
cmd = command
|
||||
config['launcher_cmd'] = cmd
|
||||
config_dirty = True
|
||||
|
||||
# 2.5 Auto-detect gamemode/game-performance
|
||||
if "use_gamemode" not in config:
|
||||
gamemode_cmd = None
|
||||
if shutil.which("game-performance"):
|
||||
gamemode_cmd = "game-performance"
|
||||
elif shutil.which("gamemoderun"):
|
||||
gamemode_cmd = "gamemoderun"
|
||||
# 2.5 Auto-detect gamemode/game-performance
|
||||
if "use_gamemode" not in config:
|
||||
gamemode_cmd = None
|
||||
if shutil.which("game-performance"):
|
||||
gamemode_cmd = "game-performance"
|
||||
elif shutil.which("gamemoderun"):
|
||||
gamemode_cmd = "gamemoderun"
|
||||
|
||||
if gamemode_cmd:
|
||||
print(f"\n'{gamemode_cmd}' was detected on your system.")
|
||||
print("Would you like to enable it for XIVLauncher? This can improve game performance.")
|
||||
try:
|
||||
choice = input(f"Enable {gamemode_cmd}? [Y/n]: ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
choice = 'n'
|
||||
print("\nPrompt interrupted, defaulting to No.")
|
||||
if gamemode_cmd:
|
||||
print(f"\n'{gamemode_cmd}' was detected on your system.")
|
||||
print("Would you like to enable it for XIVLauncher? This can improve game performance.")
|
||||
try:
|
||||
if sys.stdin.isatty():
|
||||
choice = input(f"Enable {gamemode_cmd}? [Y/n]: ").strip().lower()
|
||||
else:
|
||||
choice = 'y' # auto-enable if not interactive
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
choice = 'n'
|
||||
print("\nPrompt interrupted, defaulting to No.")
|
||||
|
||||
if choice in ('', 'y', 'yes'):
|
||||
config["use_gamemode"] = True
|
||||
config["gamemode_cmd"] = gamemode_cmd
|
||||
else:
|
||||
config["use_gamemode"] = False
|
||||
config_dirty = True
|
||||
if choice in ('', 'y', 'yes'):
|
||||
config["use_gamemode"] = True
|
||||
config["gamemode_cmd"] = gamemode_cmd
|
||||
else:
|
||||
config["use_gamemode"] = False
|
||||
config_dirty = True
|
||||
|
||||
# Save config if updated (e.g. launcher_cmd detected)
|
||||
if config_dirty:
|
||||
|
|
@ -395,18 +436,25 @@ def main():
|
|||
print("Error: No secret available.")
|
||||
sys.exit(1)
|
||||
|
||||
if args.inject_only:
|
||||
# Only run the OTP injection loop and exit
|
||||
send_otp(secret)
|
||||
sys.exit(0)
|
||||
|
||||
if args.prelaunch:
|
||||
print("Running in prelaunch mode. Spawning OTP injector in background...")
|
||||
# Spawn ourselves in background with --inject-only
|
||||
subprocess.Popen([sys.executable, os.path.realpath(__file__), '--inject-only'],
|
||||
start_new_session=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Launch Command: {cmd}")
|
||||
|
||||
# 3. Launch Logic
|
||||
try:
|
||||
# Launch the process
|
||||
# We use shell=True if the command contains arguments like 'flatpak run ...'
|
||||
# But split() and direct arg list is safer and cleaner if possible.
|
||||
# However, 'flatpak run ...' is multiple args.
|
||||
# If the user put a complex command string in config, we should respect it.
|
||||
# Simple split logic:
|
||||
import shlex
|
||||
args = shlex.split(cmd)
|
||||
if not cmd_args:
|
||||
cmd_args = shlex.split(cmd)
|
||||
|
||||
# Prepend gamemode wrapper if enabled
|
||||
if config.get("use_gamemode"):
|
||||
|
|
@ -417,16 +465,17 @@ def main():
|
|||
elif shutil.which("gamemoderun"):
|
||||
g_cmd = "gamemoderun"
|
||||
if g_cmd and shutil.which(g_cmd):
|
||||
args.insert(0, g_cmd)
|
||||
cmd_args.insert(0, g_cmd)
|
||||
print(f"Using game performance wrapper: {g_cmd}")
|
||||
|
||||
try:
|
||||
# Inject XL_SECRET_PROVIDER=file environment variable
|
||||
env = os.environ.copy()
|
||||
env['XL_SECRET_PROVIDER'] = 'file'
|
||||
|
||||
proc = subprocess.Popen(args, env=env)
|
||||
proc = subprocess.Popen(cmd_args, env=env)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Command executable not found: {cmd}")
|
||||
print(f"Error: Command executable not found: {cmd_args[0] if cmd_args else cmd}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error launching command '{cmd}': {e}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue