This plan details the implementation of version 3.0.0, introducing a dynamic splitscreen system where players can join and leave Minecraft sessions on-the-fly. The launcher will monitor controller connections, spawn new instances when players join, and reposition windows when the player count changes.
Branch Strategy: Feature development on rev3-dynamic-splitscreen branch, merged to main after validation.
- Dynamic Controller Monitoring: Continuously watch for controller hotplug events
- On-the-fly Instance Spawning: Launch new Minecraft instances when controllers connect
- Dynamic Window Repositioning: Reposition existing windows when player count changes
- Player Exit Detection: Detect when a player quits and reposition remaining windows
- Flexible Sessions: Single player can start, others join/leave freely
- Create
rev3-dynamic-splitscreenbranch frommain - Update all version references to
3.0.0 - Keep branch mergeable with ongoing
maindevelopment (Rev 2.x fixes) - Update README.md with feature documentation
- Merge to
mainonly after validation
- Layout Logic:
setSplitscreenModeForPlayer()already handles 1/2/3/4 player layouts - Instance Launching:
launchGame()function works for individual instances - Controller Detection:
getControllerCount()with Steam virtual controller filtering - Pre-created Instances:
latestUpdate-1throughlatestUpdate-4directories exist
- Controller hotplug monitoring (event-driven)
- Process tracking for running instances
- Event loop for dynamic orchestration
- Window repositioning (xdotool for X11, restart for Game Mode)
- Mode selection UI (static vs dynamic)
The splitscreen mod reads splitscreen.properties only at startup:
- Desktop Mode (X11): Use
xdotool/wmctrlto move windows externally - Game Mode (gamescope): Must restart instances with new coordinates
| File | Change Type | Description |
|---|---|---|
modules/version_info.sh |
Minor | Update SCRIPT_VERSION to "3.0.0" |
modules/launcher_script_generator.sh |
Major | Add dynamic mode logic to generated script |
README.md |
Major | Document dynamic splitscreen feature |
CLAUDE.md |
Minor | Update version references, add backlog items |
install-minecraft-splitscreen.sh |
Minor | Version display update |
All changes fit within existing module structure.
Estimated effort: 30 minutes
git checkout main
git pull origin main
git checkout -b rev3-dynamic-splitscreenFile: modules/version_info.sh
Change:
# Line 43
readonly SCRIPT_VERSION="3.0.0"Also update changelog comment:
# @changelog
# 3.0.0 (2026-XX-XX) - Dynamic splitscreen: players can join/leave mid-session
# 2.0.0 (2026-01-24) - Updated for modular installer architecture
# 1.0.0 (2026-01-22) - Initial versionUpdate @version in all module files to 3.0.0:
modules/version_info.shmodules/utilities.shmodules/path_configuration.shmodules/launcher_setup.shmodules/launcher_script_generator.shmodules/java_management.shmodules/version_management.shmodules/lwjgl_management.shmodules/mod_management.shmodules/instance_creation.shmodules/pollymc_setup.shmodules/steam_integration.shmodules/desktop_launcher.shmodules/main_workflow.sh
Estimated effort: 2-3 hours
File: modules/launcher_script_generator.sh (in generated script template)
Add after existing variable declarations (~line 112):
# =============================================================================
# Dynamic Splitscreen State (Rev 3.0.0)
# =============================================================================
declare -a INSTANCE_PIDS=("" "" "" "") # PID for each player slot (index 0-3)
declare -a INSTANCE_ACTIVE=(0 0 0 0) # 1 if slot is in use, 0 otherwise
CURRENT_PLAYER_COUNT=0 # Number of active players
DYNAMIC_MODE=0 # 1 if dynamic mode enabled
CONTROLLER_MONITOR_PID="" # PID of monitor subprocessFile: modules/launcher_script_generator.sh (in generated script template)
# Monitor controller connections/disconnections
# Writes "CONTROLLER_CHANGE:<count>" to stdout when changes detected
monitorControllers() {
local last_count
last_count=$(getControllerCount)
# Prefer inotifywait (event-driven, efficient)
if command -v inotifywait >/dev/null 2>&1; then
inotifywait -m -q -e create -e delete /dev/input/ 2>/dev/null | while read -r _ action file; do
if [[ "$file" =~ ^js[0-9]+$ ]]; then
sleep 0.5 # Debounce rapid events
local new_count
new_count=$(getControllerCount)
if [ "$new_count" != "$last_count" ]; then
echo "CONTROLLER_CHANGE:$new_count"
last_count=$new_count
fi
fi
done
else
# Fallback: poll every 2 seconds
while true; do
sleep 2
local new_count
new_count=$(getControllerCount)
if [ "$new_count" != "$last_count" ]; then
echo "CONTROLLER_CHANGE:$new_count"
last_count=$new_count
fi
done
fi
}
# Start controller monitoring in background
startControllerMonitor() {
# Create a named pipe for communication
local pipe_path="/tmp/mc-splitscreen-$$"
mkfifo "$pipe_path" 2>/dev/null || true
monitorControllers > "$pipe_path" &
CONTROLLER_MONITOR_PID=$!
# Open pipe for reading on fd 3
exec 3< "$pipe_path"
log_info "Controller monitor started (PID: $CONTROLLER_MONITOR_PID)"
}
# Stop controller monitoring
stopControllerMonitor() {
if [ -n "$CONTROLLER_MONITOR_PID" ]; then
kill "$CONTROLLER_MONITOR_PID" 2>/dev/null || true
wait "$CONTROLLER_MONITOR_PID" 2>/dev/null || true
CONTROLLER_MONITOR_PID=""
fi
# Clean up pipe
rm -f "/tmp/mc-splitscreen-$$" 2>/dev/null || true
exec 3<&- 2>/dev/null || true
}Estimated effort: 2-3 hours
File: modules/launcher_script_generator.sh (in generated script template)
# Launch a single instance for a player slot
# Arguments: $1 = slot number (1-4), $2 = total players for layout
launchInstanceForSlot() {
local slot=$1
local total_players=$2
local idx=$((slot - 1))
# Configure splitscreen position using existing function
setSplitscreenModeForPlayer "$slot" "$total_players"
# Launch the game
launchGame "latestUpdate-$slot" "P$slot" &
local pid=$!
# Track the instance
INSTANCE_PIDS[$idx]=$pid
INSTANCE_ACTIVE[$idx]=1
log_info "Launched instance $slot (PID: $pid)"
}
# Check if an instance is still running
isInstanceRunning() {
local slot=$1
local idx=$((slot - 1))
local pid="${INSTANCE_PIDS[$idx]}"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
return 1
}
# Get next available slot (1-4), returns empty if all full
getNextAvailableSlot() {
for i in 1 2 3 4; do
local idx=$((i - 1))
if [ "${INSTANCE_ACTIVE[$idx]}" = "0" ]; then
echo "$i"
return 0
fi
done
echo ""
}
# Count currently active instances
countActiveInstances() {
local count=0
for i in 0 1 2 3; do
if [ "${INSTANCE_ACTIVE[$i]}" = "1" ]; then
count=$((count + 1))
fi
done
echo "$count"
}
# Mark an instance as stopped
markInstanceStopped() {
local slot=$1
local idx=$((slot - 1))
INSTANCE_PIDS[$idx]=""
INSTANCE_ACTIVE[$idx]=0
}Estimated effort: 3-4 hours
File: modules/launcher_script_generator.sh (in generated script template)
# Check if external window management is available
canUseExternalWindowManagement() {
# Must be on X11 and have xdotool or wmctrl
if [ -z "$DISPLAY" ]; then
return 1
fi
# Not available in gamescope/Game Mode
if isSteamDeckGameMode; then
return 1
fi
# Check for tools
command -v xdotool >/dev/null 2>&1 || command -v wmctrl >/dev/null 2>&1
}
# Get window ID for a Minecraft instance by PID
getWindowIdForPid() {
local pid=$1
if command -v xdotool >/dev/null 2>&1; then
# xdotool can search by PID
xdotool search --pid "$pid" 2>/dev/null | head -1
elif command -v wmctrl >/dev/null 2>&1; then
# wmctrl needs window list parsing
wmctrl -lp 2>/dev/null | awk -v pid="$pid" '$3 == pid {print $1; exit}'
fi
}
# Move and resize a window
moveResizeWindow() {
local window_id=$1
local x=$2 y=$3 width=$4 height=$5
if [ -z "$window_id" ]; then
return 1
fi
if command -v xdotool >/dev/null 2>&1; then
xdotool windowmove "$window_id" "$x" "$y"
xdotool windowsize "$window_id" "$width" "$height"
elif command -v wmctrl >/dev/null 2>&1; then
wmctrl -i -r "$window_id" -e "0,$x,$y,$width,$height"
fi
}File: modules/launcher_script_generator.sh (in generated script template)
# Get screen dimensions
getScreenDimensions() {
local width=1920
local height=1080
if command -v xdpyinfo >/dev/null 2>&1 && [ -n "$DISPLAY" ]; then
local dims
dims=$(xdpyinfo 2>/dev/null | grep dimensions | awk '{print $2}')
if [ -n "$dims" ]; then
width=$(echo "$dims" | cut -dx -f1)
height=$(echo "$dims" | cut -dx -f2)
fi
fi
echo "$width $height"
}
# Calculate window geometry for external positioning
# Returns: x y width height
calculateWindowPosition() {
local slot=$1
local total_players=$2
local screen_width=$3
local screen_height=$4
case "$total_players" in
1)
echo "0 0 $screen_width $screen_height"
;;
2)
local half_height=$((screen_height / 2))
case "$slot" in
1) echo "0 0 $screen_width $half_height" ;;
2) echo "0 $half_height $screen_width $half_height" ;;
esac
;;
3|4)
local half_width=$((screen_width / 2))
local half_height=$((screen_height / 2))
case "$slot" in
1) echo "0 0 $half_width $half_height" ;;
2) echo "$half_width 0 $half_width $half_height" ;;
3) echo "0 $half_height $half_width $half_height" ;;
4) echo "$half_width $half_height $half_width $half_height" ;;
esac
;;
esac
}
# Reposition all active windows for new player count
repositionAllWindows() {
local new_total=$1
read -r screen_width screen_height < <(getScreenDimensions)
if canUseExternalWindowManagement; then
log_info "Repositioning windows via xdotool/wmctrl for $new_total players"
local slot_num=0
for i in 1 2 3 4; do
local idx=$((i - 1))
if [ "${INSTANCE_ACTIVE[$idx]}" = "1" ]; then
slot_num=$((slot_num + 1))
local pid="${INSTANCE_PIDS[$idx]}"
local window_id
window_id=$(getWindowIdForPid "$pid")
if [ -n "$window_id" ]; then
read -r x y w h < <(calculateWindowPosition "$slot_num" "$new_total" "$screen_width" "$screen_height")
moveResizeWindow "$window_id" "$x" "$y" "$w" "$h"
log_info "Repositioned window for slot $i to ${x},${y} ${w}x${h}"
fi
fi
done
else
log_warning "External window management not available"
log_info "Updating splitscreen.properties and restarting instances"
repositionWithRestart "$new_total"
fi
}
# Reposition by restarting instances (Game Mode fallback)
repositionWithRestart() {
local new_total=$1
# Stop all instances
for i in 1 2 3 4; do
local idx=$((i - 1))
if [ "${INSTANCE_ACTIVE[$idx]}" = "1" ]; then
local pid="${INSTANCE_PIDS[$idx]}"
if [ -n "$pid" ]; then
log_info "Stopping instance $i for repositioning"
kill "$pid" 2>/dev/null || true
fi
fi
done
# Wait for all to exit
sleep 2
# Relaunch active instances with new positions
local slot_num=0
for i in 1 2 3 4; do
local idx=$((i - 1))
if [ "${INSTANCE_ACTIVE[$idx]}" = "1" ]; then
slot_num=$((slot_num + 1))
launchInstanceForSlot "$i" "$new_total"
fi
done
}Estimated effort: 3-4 hours
File: modules/launcher_script_generator.sh (in generated script template)
# Handle controller count change
handleControllerChange() {
local new_controller_count=$1
local current_active
current_active=$(countActiveInstances)
log_info "Controller change detected: $new_controller_count controllers (currently $current_active active)"
# Add new instances if controllers increased and we have room
while [ "$current_active" -lt "$new_controller_count" ] && [ "$current_active" -lt 4 ]; do
local slot
slot=$(getNextAvailableSlot)
if [ -n "$slot" ]; then
local new_total=$((current_active + 1))
log_info "Player $new_total joining (slot $slot)"
showNotification "Player Joined" "Player $new_total is joining the game"
# Update ALL windows for new layout
repositionAllWindows "$new_total"
# Launch the new instance
launchInstanceForSlot "$slot" "$new_total"
current_active=$new_total
else
break
fi
done
CURRENT_PLAYER_COUNT=$current_active
}
# Check for and handle exited instances
checkForExitedInstances() {
local any_exited=0
for i in 1 2 3 4; do
local idx=$((i - 1))
if [ "${INSTANCE_ACTIVE[$idx]}" = "1" ]; then
if ! isInstanceRunning "$i"; then
log_info "Player $i has exited"
showNotification "Player Left" "Player $i has left the game"
markInstanceStopped "$i"
any_exited=1
fi
fi
done
if [ "$any_exited" = "1" ]; then
local remaining
remaining=$(countActiveInstances)
CURRENT_PLAYER_COUNT=$remaining
if [ "$remaining" -gt 0 ]; then
log_info "Repositioning for $remaining remaining players"
repositionAllWindows "$remaining"
fi
fi
}
# Show desktop notification
showNotification() {
local title="$1"
local message="$2"
if command -v notify-send >/dev/null 2>&1; then
notify-send -a "Minecraft Splitscreen" "$title" "$message" 2>/dev/null || true
fi
}File: modules/launcher_script_generator.sh (in generated script template)
# Run dynamic splitscreen mode
runDynamicSplitscreen() {
log_info "Starting dynamic splitscreen mode"
DYNAMIC_MODE=1
local instances_ever_launched=0
# Start controller monitoring
startControllerMonitor
# Initial launch based on current controllers
local initial_count
initial_count=$(getControllerCount)
if [ "$initial_count" -gt 0 ]; then
handleControllerChange "$initial_count"
instances_ever_launched=1
else
log_info "No controllers detected. Waiting for controller connection..."
showNotification "Waiting for Controllers" "Connect a controller to start playing"
fi
# Main event loop
while true; do
# Check for controller events (non-blocking read with timeout)
if read -t 1 -u 3 event 2>/dev/null; then
if [[ "$event" =~ ^CONTROLLER_CHANGE:([0-9]+)$ ]]; then
handleControllerChange "${BASH_REMATCH[1]}"
instances_ever_launched=1
fi
fi
# Check for exited instances
checkForExitedInstances
# Exit if all players have left (and at least one ever played)
local active
active=$(countActiveInstances)
if [ "$active" -eq 0 ] && [ "$instances_ever_launched" = "1" ]; then
log_info "All players have exited. Ending session."
break
fi
done
# Cleanup
stopControllerMonitor
log_info "Dynamic splitscreen session ended"
}
# Run static splitscreen mode (original behavior)
runStaticSplitscreen() {
log_info "Starting static splitscreen mode"
DYNAMIC_MODE=0
local numberOfControllers
numberOfControllers=$(getControllerCount)
echo "[Info] Detected $numberOfControllers controller(s), launching splitscreen instances..."
for player in $(seq 1 "$numberOfControllers"); do
setSplitscreenModeForPlayer "$player" "$numberOfControllers"
echo "[Info] Launching instance $player of $numberOfControllers (latestUpdate-$player)"
launchGame "latestUpdate-$player" "P$player"
done
echo "[Info] All instances launched. Waiting for games to exit..."
wait
echo "[Info] All games have exited."
}File: modules/launcher_script_generator.sh (in generated script template)
Update main entry point:
# =============================================================================
# MAIN ENTRY POINT
# =============================================================================
# Enable debug output with SPLITSCREEN_DEBUG=1
if [ "${SPLITSCREEN_DEBUG:-0}" = "1" ]; then
echo "[Debug] === Minecraft Splitscreen Launcher v__SCRIPT_VERSION__ ===" >&2
echo "[Debug] Launcher: $LAUNCHER_NAME ($LAUNCHER_TYPE)" >&2
echo "[Debug] Instances: $INSTANCES_DIR" >&2
echo "[Debug] Environment: XDG_CURRENT_DESKTOP=$XDG_CURRENT_DESKTOP DISPLAY=$DISPLAY" >&2
fi
# Mode selection (skip if launched with argument)
LAUNCH_MODE="${1:-}"
if [ -z "$LAUNCH_MODE" ] && [ "$1" != "launchFromPlasma" ]; then
echo ""
echo "=== Minecraft Splitscreen Launcher v__SCRIPT_VERSION__ ==="
echo ""
echo "Launch Modes:"
echo " 1. Static - Launch based on current controllers (original behavior)"
echo " 2. Dynamic - Players can join/leave during session [NEW]"
echo ""
read -t 10 -p "Select mode [1]: " mode_choice || mode_choice=""
mode_choice=${mode_choice:-1}
case "$mode_choice" in
2|dynamic|d) LAUNCH_MODE="dynamic" ;;
*) LAUNCH_MODE="static" ;;
esac
fi
if isSteamDeckGameMode; then
if [ "$1" = "launchFromPlasma" ]; then
# Inside nested Plasma session
rm -f ~/.config/autostart/minecraft-launch.desktop
if [ "$LAUNCH_MODE" = "dynamic" ]; then
runDynamicSplitscreen
else
launchGames
fi
qdbus org.kde.Shutdown /Shutdown org.kde.Shutdown.logout
else
# Start nested session (pass mode)
nestedPlasma "$LAUNCH_MODE"
fi
else
# Desktop mode
if [ "$LAUNCH_MODE" = "dynamic" ]; then
runDynamicSplitscreen
else
runStaticSplitscreen
fi
fiEstimated effort: 2-3 hours
Add new section after "Features":
## Dynamic Splitscreen Mode (v3.0.0)
Version 3.0.0 introduces **Dynamic Splitscreen** - players can now join and leave mid-session without everyone needing to start at the same time.
### How It Works
1. **Launch the game** - Choose "Dynamic" mode when prompted
2. **Start playing** - The first controller detected launches Player 1 in fullscreen
3. **Players join** - When a new controller connects, a new Minecraft instance launches and all windows reposition automatically
4. **Players leave** - When a player quits Minecraft, remaining windows expand to use the available space
5. **Session ends** - When all players have exited, the launcher closes
### Window Repositioning
The system automatically repositions windows based on player count:
- **1 player**: Fullscreen
- **2 players**: Top/Bottom split
- **3-4 players**: Quad split (2x2 grid)
**Desktop Mode (X11)**: Uses `xdotool` or `wmctrl` for smooth, non-disruptive window repositioning.
**Steam Deck Game Mode**: Restarts instances with new positions (the splitscreen mod only reads configuration at startup).
### Requirements for Dynamic Mode
For the best experience, install these optional packages:
```bash
# Debian/Ubuntu
sudo apt install inotify-tools xdotool wmctrl libnotify-bin
# Fedora
sudo dnf install inotify-tools xdotool wmctrl libnotify
# Arch
sudo pacman -S inotify-tools xdotool wmctrl libnotifyinotify-tools: Efficient controller hotplug detection (falls back to polling if unavailable)xdotool/wmctrl: Smooth window repositioning on X11libnotify: Desktop notifications when players join/leave
- Wayland: External window management may not work on pure Wayland; XWayland apps typically work
- Game Mode: Window repositioning requires restarting instances (brief interruption)
- Maximum 4 players: Hardware and mod limitation
Update "Recent Improvements" section:
```markdown
## Recent Improvements
- ✅ **Dynamic Splitscreen (v3.0.0)**: Players can join and leave mid-session - no need for everyone to start at the same time
- ✅ **Controller Hotplug**: Real-time detection of controller connections/disconnections
- ✅ **Automatic Window Repositioning**: Windows automatically resize when player count changes
- ✅ **Desktop Notifications**: Get notified when players join or leave
- [existing items...]
Update version references and add to backlog:
**Version:** 3.0.0Add to Active Development Backlog:
### Issue #5: Dynamic Splitscreen Mode (v3.0.0) - IMPLEMENTED
**Feature:** Players can join and leave mid-session without coordinating start times.
**Technical Implementation:**
- Controller monitoring via `inotifywait` with polling fallback
- Process tracking with PID arrays for 4 instance slots
- External window repositioning via `xdotool`/`wmctrl` on X11
- Instance restart fallback for Game Mode
- Event loop architecture in generated launcher script
**Files modified:**
- `modules/launcher_script_generator.sh` - Major changes for dynamic mode
- `modules/version_info.sh` - Version bump to 3.0.0
- `README.md` - Feature documentation
- All module headers - Version updateEstimated effort: 4-6 hours
| Scenario | Environment | Expected Behavior |
|---|---|---|
| 1 controller start, 2nd joins | Desktop X11 | Spawn P2, reposition via xdotool |
| 1 controller start, 2nd joins | Steam Deck Game Mode | Spawn P2, restart P1 with new position |
| P2 quits Minecraft | All | Reposition P1 to fullscreen |
| Controller disconnect (player still in game) | All | No action (avoid false positives) |
| 4 players, P2 quits | All | Reposition remaining 3 players |
| No controllers at start | All | Wait for controller, show notification |
| Static mode selected | All | Original behavior unchanged |
inotifywait not installed |
All | Fall back to polling |
xdotool not installed |
Desktop | Log warning, use restart method |
Before merging to main:
- Rebase onto latest
mainto incorporate Rev 2.x fixes - Run full test matrix
- Test fresh installation via
curl | bash - Test upgrade from Rev 2.x installation
- Verify version displays correctly throughout
main (2.0.x) ─────────────────────────────────────────► (continues)
│ │
└── rev3-dynamic-splitscreen (3.0.0) ─────────────────┘
(merge when validated)
# Periodically sync with main
git checkout rev3-dynamic-splitscreen
git fetch origin
git rebase origin/main
# Resolve any conflicts
git push --force-with-lease# After validation
git checkout main
git merge --no-ff rev3-dynamic-splitscreen -m "feat: Dynamic splitscreen mode (v3.0.0)"
git tag v3.0.0
git push origin main --tags- Bash 4.0+
- Standard Linux utilities
- PollyMC launcher
| Package | Purpose | Fallback |
|---|---|---|
inotify-tools |
Efficient controller event detection | Polling every 2 seconds |
xdotool |
X11 window positioning | Instance restart |
wmctrl |
Alternative X11 window management | Instance restart |
libnotify |
Desktop notifications | Silent operation |
| Risk | Severity | Mitigation |
|---|---|---|
| Splitscreen mod doesn't reload config | HIGH | Use external window tools on X11; restart on Game Mode |
| Wayland incompatibility | MEDIUM | Document limitation; most gaming distros use X11 |
| Steam virtual controller issues | LOW | Existing getControllerCount() filtering handles this |
| Rev 2.x merge conflicts | LOW | Modular architecture isolates changes to launcher_script_generator.sh |
| Phase | Effort | Risk |
|---|---|---|
| 1. Branch Setup & Version | 30 min | Low |
| 2. Controller Monitoring | 2-3 hours | Medium |
| 3. Instance Lifecycle | 2-3 hours | Low |
| 4. Window Repositioning | 3-4 hours | High |
| 5. Main Event Loop | 3-4 hours | Medium |
| 6. Documentation | 2-3 hours | Low |
| 7. Testing | 4-6 hours | Medium |
| Total | 17-24 hours | Medium-High |
- Single player can start and play alone
- New controller connection launches new instance automatically
- Windows reposition correctly for 1/2/3/4 player configurations
- Player quitting Minecraft triggers repositioning of remaining windows
- Works on Steam Deck Desktop Mode with xdotool
- Works on Steam Deck Game Mode with restart method
- Static mode unchanged from Rev 2.x behavior
- Version 3.0.0 displays correctly at install and runtime
- README documents the feature completely
- Branch merges cleanly with ongoing main development
- All existing tests continue to pass
This plan is ready for implementation. Do you want me to:
- Proceed with this plan - Start with Phase 1 (branch creation and version updates)
- Modify the plan - Adjust scope, ordering, or approach
- Ask clarifying questions - Get more details on specific aspects