Mohammed Chami
.NET Developer | Content Creator
Mohammed Chami
.NET Developer | Content Creator

Think of shell scripts as the “conductor” of an orchestra. Just like a conductor coordinates different musicians to create beautiful music, a shell script coordinates different technologies (Wine, containers, graphics drivers) to run your Windows game on Linux seamlessly.
In the game distribution system you found, shell scripts are the glue that binds everything together:
User clicks → Shell Script → Magic happens → Game runs
The script handles:
Let’s break down what those start.{n/e-w/n-w}.sh scripts actually do by building one step by step.
#!/bin/bash
# The shebang tells Linux this is a bash script
# Script naming convention from your document:
# start.n.sh = Native Linux build
# start.e-w.sh = Emulation with Wine
# start.n-w.sh = Native with Wine fallback
set -e # Exit on any error
set -u # Exit on undefined variables
# Global variables
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GAME_DIR="${SCRIPT_DIR}"
WINE_PREFIX="${HOME}/.local/share/wineprefixes/$(basename "${GAME_DIR}")"
The script needs to understand the system it’s running on:
detect_system() {
# Detect distribution
if command -v pacman >/dev/null 2>&1; then
DISTRO="arch"
elif command -v apt >/dev/null 2>&1; then
DISTRO="debian"
elif command -v dnf >/dev/null 2>&1; then
DISTRO="fedora"
else
DISTRO="unknown"
echo "Warning: Unknown distribution"
fi
# Detect graphics driver
if lspci | grep -i nvidia >/dev/null; then
GPU_VENDOR="nvidia"
# Check if proprietary driver is loaded
if lsmod | grep nvidia >/dev/null; then
GPU_DRIVER="proprietary"
else
GPU_DRIVER="nouveau"
fi
elif lspci | grep -i amd >/dev/null; then
GPU_VENDOR="amd"
GPU_DRIVER="mesa"
elif lspci | grep -i intel >/dev/null; then
GPU_VENDOR="intel"
GPU_DRIVER="mesa"
fi
# Detect audio system
if pgrep -x pipewire >/dev/null; then
AUDIO_SYSTEM="pipewire"
elif pgrep -x pulseaudio >/dev/null; then
AUDIO_SYSTEM="pulseaudio"
else
AUDIO_SYSTEM="alsa"
fi
echo "Detected: $DISTRO, GPU: $GPU_VENDOR ($GPU_DRIVER), Audio: $AUDIO_SYSTEM"
}
Ensure all required components are installed:
check_dependencies() {
local missing_deps=()
# Required commands
local required_commands=(
"wine" # Wine compatibility layer
"dwarfs" # Compressed filesystem
"bwrap" # Bubblewrap sandboxing
"fuse-overlayfs" # Overlay filesystem
)
for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing_deps+=("$cmd")
fi
done
# Graphics-specific dependencies
case "$GPU_VENDOR" in
"nvidia")
if [ "$GPU_DRIVER" = "proprietary" ]; then
# Check for NVIDIA proprietary drivers
if ! ldconfig -p | grep -q "libnvidia-gl-core"; then
missing_deps+=("nvidia-utils")
fi
fi
;;
"amd"|"intel")
# Check for Mesa drivers
if ! ldconfig -p | grep -q "libGL.so"; then
missing_deps+=("mesa")
fi
;;
esac
# Report missing dependencies
if [ ${#missing_deps[@]} -gt 0 ]; then
echo "Error: Missing dependencies: ${missing_deps[*]}"
echo "Please install them using your package manager"
exit 1
fi
}
Configure Wine for optimal game performance:
setup_wine() {
# Create Wine prefix if it doesn't exist
if [ ! -d "$WINE_PREFIX" ]; then
echo "Creating Wine prefix at $WINE_PREFIX"
WINEPREFIX="$WINE_PREFIX" winecfg
fi
# Configure Wine for gaming
export WINEPREFIX="$WINE_PREFIX"
export WINEDEBUG=-all # Disable debug output for performance
# Set Windows version (usually Windows 10 for modern games)
WINEPREFIX="$WINE_PREFIX" winecfg -v win10
# Install common game dependencies via winetricks
if command -v winetricks >/dev/null 2>&1; then
echo "Installing common game dependencies..."
WINEPREFIX="$WINE_PREFIX" winetricks -q \
vcrun2019 \ # Visual C++ 2019 redistributable
d3dcompiler_47 \ # DirectX shader compiler
dxvk \ # DirectX to Vulkan translation
win10 # Windows 10 compatibility
fi
}
Mount filesystems and prepare the container environment:
setup_container() {
# Create temporary directories
local temp_base="/tmp/game-$(basename "$GAME_DIR")-$$"
GAME_MOUNT="$temp_base/game"
OVERLAY_WORK="$temp_base/work"
OVERLAY_UPPER="$temp_base/upper"
OVERLAY_MERGED="$temp_base/merged"
mkdir -p "$GAME_MOUNT" "$OVERLAY_WORK" "$OVERLAY_UPPER" "$OVERLAY_MERGED"
# Mount DwarFS archive if it exists
if [ -f "$GAME_DIR/game.dwarfs" ]; then
echo "Mounting compressed game archive..."
dwarfs "$GAME_DIR/game.dwarfs" "$GAME_MOUNT"
else
# Fallback to direct directory access
mount --bind "$GAME_DIR" "$GAME_MOUNT"
fi
# Create overlay filesystem
echo "Setting up overlay filesystem..."
mount -t overlay overlay \
-o lowerdir="$GAME_MOUNT:$WINE_PREFIX",upperdir="$OVERLAY_UPPER",workdir="$OVERLAY_WORK" \
"$OVERLAY_MERGED"
# Cleanup function to unmount on exit
cleanup() {
echo "Cleaning up..."
umount "$OVERLAY_MERGED" 2>/dev/null || true
umount "$GAME_MOUNT" 2>/dev/null || true
rm -rf "$temp_base"
}
trap cleanup EXIT
}
Configure optimal settings for different hardware:
setup_graphics() {
# Graphics environment variables
case "$GPU_VENDOR" in
"nvidia")
if [ "$GPU_DRIVER" = "proprietary" ]; then
export __GL_SHADER_DISK_CACHE=1
export __GL_SHADER_DISK_CACHE_PATH="$WINE_PREFIX/shader_cache"
export __GL_THREADED_OPTIMIZATIONS=1
# Enable DRM kernel mode setting for Gamescope
export __GL_GSYNC_ALLOWED=0
export __GL_VRR_ALLOWED=0
fi
;;
"amd")
export RADV_PERFTEST=aco,llvm
export AMD_VULKAN_ICD=RADV
;;
"intel")
export INTEL_DEBUG=norbc
;;
esac
# Enable Vulkan if available
if vulkaninfo >/dev/null 2>&1; then
export VK_ICD_FILENAMES="/usr/share/vulkan/icd.d/*"
echo "Vulkan support detected"
fi
}
setup_audio() {
case "$AUDIO_SYSTEM" in
"pipewire")
export PULSE_RUNTIME_PATH="${XDG_RUNTIME_DIR}/pulse"
;;
"pulseaudio")
# Ensure PulseAudio is running
if ! pgrep -x pulseaudio >/dev/null; then
pulseaudio --start
fi
;;
"alsa")
# Use ALSA directly
export ALSA_CARD=0
;;
esac
}
Set up Gamescope for better game window management:
setup_gamescope() {
# Read configuration from user file
local config_file="$HOME/.jc141rc"
if [ -f "$config_file" ]; then
source "$config_file"
fi
# Default Gamescope settings
local gamescope_args=(
--rt # Real-time priority
--steam # Steam compatibility mode
-W 1920 -H 1080 # Window resolution
-w 1920 -h 1080 # Game resolution
)
# Add user-defined additional flags
if [ -n "${ADDITIONAL_FLAGS:-}" ]; then
read -ra user_flags <<< "$ADDITIONAL_FLAGS"
gamescope_args+=("${user_flags[@]}")
fi
# Use Gamescope if available and configured
if command -v gamescope >/dev/null 2>&1 && [ "${USE_GAMESCOPE:-0}" = "1" ]; then
LAUNCHER="gamescope ${gamescope_args[*]} --"
else
LAUNCHER=""
fi
}
Finally, launch the game with all systems prepared:
launch_game() {
local game_exe="$1"
echo "Launching game: $game_exe"
echo "Wine prefix: $WINE_PREFIX"
echo "Container root: $OVERLAY_MERGED"
# Build bubblewrap command
local bwrap_args=(
--dev-bind /dev /dev # Device access
--proc /proc # Process filesystem
--ro-bind /usr /usr # Read-only system libraries
--ro-bind /etc /etc # System configuration
--bind "$HOME/.local/share" "$HOME/.local/share" # User data
--ro-bind "$OVERLAY_MERGED" /game # Game files
--tmpfs /tmp # Temporary filesystem
--unshare-pid # Process isolation
--setenv WINEPREFIX "$WINE_PREFIX" # Wine configuration
--setenv WINEDEBUG "-all" # Disable Wine debug
--chdir /game # Change to game directory
)
# Add graphics device access
if [ -d /dev/dri ]; then
bwrap_args+=(--dev-bind /dev/dri /dev/dri)
fi
# Launch with or without Gamescope
exec bwrap "${bwrap_args[@]}" \
$LAUNCHER wine "$game_exe"
}
main() {
echo "=== Game Launcher Starting ==="
# Find game executable
local game_exe
if [ -f "$GAME_DIR/game.exe" ]; then
game_exe="game.exe"
elif [ -f "$GAME_DIR"/*.exe ]; then
game_exe=$(basename "$GAME_DIR"/*.exe | head -n1)
else
echo "Error: No game executable found"
exit 1
fi
echo "Detected game executable: $game_exe"
# Run setup functions
detect_system
check_dependencies
setup_wine
setup_container
setup_graphics
setup_audio
setup_gamescope
# Launch the game
launch_game "$game_exe"
}
# Script entry point
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
The script can read user configuration from ~/.jc141rc:
# ~/.jc141rc - User configuration file
# Gamescope settings
USE_GAMESCOPE=1
ADDITIONAL_FLAGS="--force-grab-cursor --rt"
# Wine settings
WINE_VERSION="wine-tkg-staging-fsync-git"
WINEDEBUG="-all"
# Graphics settings
DXVK_ENABLED=1
VKD3D_ENABLED=1
# Audio settings
PULSE_LATENCY_MSEC=60
# Game-specific overrides
GAME_MyAwesomeGame_RESOLUTION="1920x1080"
GAME_MyAwesomeGame_FULLSCREEN=1
Professional scripts include robust error handling:
# Logging function
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$HOME/.local/share/game-launcher.log"
}
# Error handling
handle_error() {
local exit_code=$?
local line_number=$1
log "ERROR" "Script failed at line $line_number with exit code $exit_code"
# Try to provide helpful error messages
case $exit_code in
127)
log "ERROR" "Command not found. Check dependencies."
;;
1)
log "ERROR" "General error. Check system requirements."
;;
*)
log "ERROR" "Unknown error occurred."
;;
esac
cleanup
exit $exit_code
}
trap 'handle_error $LINENO' ERR
From your document, different script names serve different purposes:
start.n.sh (Native)# For games that have native Linux versions
main() {
detect_system
check_dependencies
setup_container
# Launch native executable directly
exec bwrap "${bwrap_args[@]}" ./MyGame
}
start.e-w.sh (Emulation with Wine)# For Windows-only games
main() {
detect_system
check_dependencies
setup_wine # Wine setup required
setup_container
setup_graphics
setup_audio
# Launch through Wine
launch_game "MyGame.exe"
}
start.n-w.sh (Native with Wine fallback)# Try native first, fall back to Wine
main() {
if [ -f "./MyGame" ]; then
# Native version available
exec ./MyGame
elif [ -f "./MyGame.exe" ]; then
# Fall back to Wine
setup_wine
launch_game "MyGame.exe"
else
echo "No executable found"
exit 1
fi
}
# Always quote variables
GAME_DIR="$1" # Good
GAME_DIR=$1 # Bad - can break with spaces
# Validate input
if [[ ! "$GAME_DIR" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then
echo "Invalid game directory"
exit 1
fi
# Use absolute paths
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Cache expensive operations
if [ ! -f "$HOME/.cache/gpu-info" ]; then
detect_gpu > "$HOME/.cache/gpu-info"
fi
source "$HOME/.cache/gpu-info"
# Use built-in commands when possible
# Good: [[ -f "$file" ]]
# Bad: test -f "$file"
# Use functions for repeated code
setup_common_env() {
export LANG=C
export LC_ALL=C
umask 022
}
# Document complex sections
# This section handles the case where the user has
# multiple Wine installations and we need to select
# the correct one for this specific game
select_wine_version() {
# ... complex logic here
}
Your shell script can integrate with desktop environments:
.desktop File:[Desktop Entry]
Name=My Awesome Game
Comment=Play My Awesome Game
Exec=/path/to/game/start.e-w.sh
Icon=/path/to/game/icon.png
Terminal=false
Type=Application
Categories=Game;
# Add as non-Steam game
# Steam will call: start.e-w.sh %command%
{
"executable": "/path/to/game/start.e-w.sh",
"platform": "linux"
}
if [ "${DEBUG:-0}" = "1" ]; then
set -x # Print every command
export WINEDEBUG="+all"
fi
if [ "${DRY_RUN:-0}" = "1" ]; then
echo "Would run: wine MyGame.exe"
exit 0
fi
# Test with different Wine versions
WINE_VERSION=wine-staging ./start.e-w.sh
# Test with different graphics drivers
GPU_DRIVER_OVERRIDE=mesa ./start.e-w.sh
# Test without Gamescope
USE_GAMESCOPE=0 ./start.e-w.sh
Shell scripts are the backbone of professional Linux game distribution. They orchestrate complex systems (Wine, containers, graphics drivers) into a seamless user experience. Understanding shell scripting allows you to:
The scripts might seem complex, but they’re just coordinating the technologies we discussed earlier. Each function has a specific purpose in creating the perfect environment for your Windows game to run on Linux.