Gaming
UCH EvenMorePlayers on macOS
Installing UCH-EvenMorePlayers on macOS: BepInEx Wrapper for Apple Silicon
Overview
This article walks through installing the UCH-EvenMorePlayers BepInEx mod1 on a Mac. The mod's official README covers Windows and Linux only; this guide fills the macOS gap. Instead of editing Steam launch options, we wrap the game's .app binary so Steam launches BepInEx through the normal flow, and we force the game under Rosetta because BepInEx 5.4.23.5's MonoMod patches crash on arm64 Mono during preload.
Verified on macOS, Apple M4, UCH 1.9.03, mod 10.0.1, BepInEx 5.4.23.5. All players in the lobby still need the mod installed, same as on Windows.
Before You Begin
Prerequisites
- Ultimate Chicken Horse installed via Steam.
- Rosetta installed (Apple Silicon only).
Check Rosetta:
arch -x86_64 /usr/bin/true && echo "Rosetta OK" If that fails, install with softwareupdate --install-rosetta --agree-to-license.
Why This Approach
Two observations from the install drive the design:
- The BepInEx 5 README's macOS guidance edits Steam launch options. Wrapping the binary inside the
.appbundle is cleaner — Steam launches BepInEx through its normal flow with no Steam-side changes. - On Apple Silicon, BepInEx 5.4.23.5's
MonoMod.RuntimeDetourcrashes during preload when patching arm64 Mono (DetourHelper.GetIdentifiablenull deref insideHarmonyInteropFix). The wrapper forces the game under Rosetta (x86_64), where MonoMod works.
The install is fully reversible from Steam. The only file we change that Steam tracks is the Mach-O at Contents/MacOS/Ultimate Chicken Horse — right-click the game in Steam → Properties → Installed Files → Verify integrity of game files to restore it at any time. The BepInEx tree and libdoorstop.dylib are extras Steam ignores; delete them manually for a clean uninstall.
How This Works
A macOS .app is a folder with a specific layout. Inside, Contents/MacOS/ holds the real executable — the file the system actually runs when Steam launches the game. BepInEx, the modding framework we're installing, works by attaching to the game process at startup and loading a plugin DLL (the MorePlayers mod itself) into the game's runtime.
To get BepInEx attached, the game needs a handful of environment variables set (the Doorstop hooks) before it launches. The cleanest way to do that on macOS is to replace the executable inside the .app with a small shell script that sets those variables and then runs the original binary. We move the original aside as Ultimate Chicken Horse.real so the script can exec it. Steam launches our shell script the same way it would launch the binary — it can't tell the difference.
The Rosetta detour is a separate problem: on Apple Silicon, BepInEx 5.4.23.5's runtime patcher crashes when running natively on arm64 Mono. So the wrapper script also forces the game to run under x86_64 emulation via arch -x86_64. Intel Macs skip the emulation and exec the binary directly.
Stage BepInEx and the Mod
Paths
Set three shell variables we'll reuse throughout the install: $GAME is the game's install directory inside Steam's library, $APP is the macOS application bundle inside it (the folder we'll modify — Finder shows it as a single icon, but it's actually a directory), and $WORK is a scratch directory under /tmp where we'll stage downloads. /tmp empties on reboot, so anything we leave there is safely discardable.
GAME="$HOME/Library/Application Support/Steam/steamapps/common/Ultimate Chicken Horse"
APP="$GAME/UltimateChickenHorse.app"
WORK="/tmp/uch-mod" Download Archives
Create the scratch directory, download two zip files — BepInEx (the modding framework, from the official BepInEx GitHub release) and the MorePlayers mod itself (from the mod author's release) — and unpack each into its own subfolder. The -o flag on unzip tells it to overwrite without prompting, which keeps the step safe to re-run.
mkdir -p "$WORK" && cd "$WORK"
curl -sSL -o bepinex.zip \
https://github.com/BepInEx/BepInEx/releases/download/v5.4.23.5/BepInEx_macos_universal_5.4.23.5.zip
curl -sSL -o mod.zip \
https://github.com/batram/UCH-EvenMorePlayers/releases/download/10.0.1/MorePlayersMod-10.0.1.zip
mkdir -p bepinex mod
unzip -oq bepinex.zip -d bepinex
unzip -oq mod.zip -d mod Copy Into the Game Folder
Copy the BepInEx framework folder and the libdoorstop.dylib loader library into the game's install directory (next to the .app bundle, not inside it). Then place the MorePlayers DLL into BepInEx's plugins folder — BepInEx scans that folder at game startup and loads any DLL it finds there.
cp -R "$WORK/bepinex/BepInEx" "$GAME/"
cp "$WORK/bepinex/libdoorstop.dylib" "$GAME/"
mkdir -p "$GAME/BepInEx/plugins/MorePlayersMod"
cp "$WORK/mod/BepInEx/plugins/MorePlayersMod/MorePlayersMod.dll" \
"$GAME/BepInEx/plugins/MorePlayersMod/" Wrap the App Binary
The game's .app launches a real binary at Contents/MacOS/Ultimate Chicken Horse. We move it aside, replace it with a same-named shell-script wrapper that sets Doorstop env vars and re-execs under Rosetta, then re-sign the bundle so macOS will still launch it.
Move the Real Binary Aside
We rename the game's executable so we can put our shell-script wrapper in its place. $BIN is the path to the real executable inside the bundle (the file Steam launches when you click Play), and $BIN.real is where we stash the original so the wrapper can re-exec it later.
The block below is self-correcting — it inspects the current state of both files and only moves a real Mach-O binary into place. That means you can safely run it from any starting state (fresh install, after Steam Verify, after a developer update, or recovering from a previous botched run) and end up wrapped. The convergence rules:
$BIN.realexists but is not a Mach-O → stale shell-script wrapper from a previous bad run; remove it.$BINis a Mach-O → it's the live binary (fresh install, Steam-restored, or just shipped via a game update); move it to.real, overwriting any older backup so the wrapper always re-execs the current version.$BINis not a Mach-O and$BIN.realis a Mach-O → already wrapped; leave.realalone and let the next step refresh$BIN.- Neither is a Mach-O → the original binary is gone; the script exits and tells you to run Steam Verify Integrity, then re-run.
The helper is_mach_o returns true when the given file is a Mach-O executable — that's macOS's native binary format, the thing Steam ships and the wrapper eventually launches.
BIN="$APP/Contents/MacOS/Ultimate Chicken Horse"
is_mach_o() {
[ -e "$1" ] && file -b "$1" 2>/dev/null | grep -q 'Mach-O'
}
if [ -e "$BIN.real" ] && ! is_mach_o "$BIN.real"; then
echo "Stale $BIN.real (not a Mach-O binary) — removing." >&2
rm -f "$BIN.real"
fi
if is_mach_o "$BIN"; then
mv -f "$BIN" "$BIN.real"
elif is_mach_o "$BIN.real"; then
echo "Already wrapped — keeping $BIN.real, refreshing wrapper." >&2
else
echo "No Mach-O binary at $BIN or $BIN.real." >&2
echo "Verify game files in Steam (right-click → Properties → Installed Files → Verify integrity), then re-run." >&2
exit 1
fi Write the Wrapper Script
Write a shell script at the location where the game's binary used to live. The script sets the Doorstop environment variables that BepInEx needs, then prepends libdoorstop.dylib to DYLD_INSERT_LIBRARIES so the dynamic linker loads it into the game process when the binary starts. Finally, it re-execs the real binary — the Apple* branch of the case statement is the validated Apple Silicon path (forces x86_64 via Rosetta), and the fallback *) branch is the upstream pattern carried over for older Intel Macs.
The heredoc syntax <<'EOF' with the quoted EOF preserves the script body byte-for-byte — no variable expansion happens at write time, so things like $GAME_DIR stay as literal text in the wrapper and resolve at game-launch time, not now.
cat > "$BIN" <<'EOF'
#!/bin/sh
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GAME_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
REAL_BIN="$SCRIPT_DIR/Ultimate Chicken Horse.real"
export DOORSTOP_ENABLED="1"
export DOORSTOP_TARGET_ASSEMBLY="$GAME_DIR/BepInEx/core/BepInEx.Preloader.dll"
export DOORSTOP_BOOT_CONFIG_OVERRIDE=""
export DOORSTOP_IGNORE_DISABLED_ENV="0"
export DOORSTOP_MONO_DLL_SEARCH_PATH_OVERRIDE=""
export DOORSTOP_MONO_DEBUG_ENABLED="0"
export DOORSTOP_MONO_DEBUG_ADDRESS="127.0.0.1:10000"
export DOORSTOP_MONO_DEBUG_SUSPEND="0"
export DOORSTOP_CLR_RUNTIME_CORECLR_PATH=".dylib"
export DOORSTOP_CLR_CORLIB_DIR=""
export DYLD_LIBRARY_PATH="$GAME_DIR:${DYLD_LIBRARY_PATH}"
if [ -z "$DYLD_INSERT_LIBRARIES" ]; then
export DYLD_INSERT_LIBRARIES="$GAME_DIR/libdoorstop.dylib"
else
export DYLD_INSERT_LIBRARIES="$GAME_DIR/libdoorstop.dylib:$DYLD_INSERT_LIBRARIES"
fi
cpu_type="$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo)"
case "$cpu_type" in
Apple*)
export ARCHPREFERENCE="x86_64"
exec arch -x86_64 -e DYLD_INSERT_LIBRARIES="$DYLD_INSERT_LIBRARIES" \
"$REAL_BIN" "$@"
;;
*)
exec "$REAL_BIN" "$@"
;;
esac
EOF
chmod +x "$BIN" Re-sign the Bundle
Modifying any file inside an .app bundle invalidates its code signature, which would otherwise make macOS refuse to launch it ("the application is damaged" dialog). The command below applies an ad-hoc signature (--sign -) over the bundle recursively (--deep), replacing whatever was there before (--force). Ad-hoc means there's no Developer ID attached, which is fine for local use.
codesign --force --deep --sign - "$APP" Launch and Verify
Restart Steam
If Steam was running during the install, fully quit it (Activity Monitor → end all Steam processes) and start it again. Without this, SteamAPI_Init() fails inside the wrapped game and the More button spins forever on a loading screen.
Check the BepInEx Log
Launch UCH from your Steam library and wait until the main menu appears — by then, BepInEx has loaded and written a log file. Tail the last few lines to confirm the framework attached and the MorePlayers mod loaded cleanly:
tail "$GAME/BepInEx/LogOutput.log" Expected:
[Info : BepInEx] Loading [EvenMorePlayers 10.0.1]
[Message: BepInEx] Chainloader startup complete If the Loading [EvenMorePlayers ...] line is missing, BepInEx attached but didn't find the mod DLL — re-check the copy step. If there's no log file at all, the wrapper didn't run; the most common cause is that Steam wasn't fully quit before re-launching (see the previous step).
Open the More Lobby
In the game, click Play Online, then More — that's the entry point the mod adds for 5–8 player lobbies.
Tail Logs Live (Optional)
Useful when something doesn't work and you want to see what the mod is doing. The script tails both the BepInEx log and the Unity player log, prefixing each line so you can tell them apart, and greps the Unity log for relevant keywords.
cat > /tmp/uch-watch.sh <<'EOF'
#!/bin/sh
GAME="$HOME/Library/Application Support/Steam/steamapps/common/Ultimate Chicken Horse"
BEP="$GAME/BepInEx/LogOutput.log"
UNI="$HOME/Library/Logs/Clever Endeavour Games/Ultimate Chicken Horse/Player.log"
touch "$BEP" "$UNI"
( tail -F "$BEP" 2>/dev/null | sed -l 's/^/[BEP] /' ) &
( tail -F "$UNI" 2>/dev/null \
| grep --line-buffered -iE 'moreplayers|bepinex|doorstop|exception|error|fail|warn|nullref|stack|braincloud|gamespark|lobby|server|match|connect' \
| sed -l 's/^/[UNI] /' ) &
wait
EOF
chmod +x /tmp/uch-watch.sh Run /tmp/uch-watch.sh in a separate terminal while the game is open.
Next Steps
With the mod loaded, share these instructions with everyone joining your lobby — all players need the mod installed for the More lobby to populate.
Resources
Footnotes
batram, "UCH-EvenMorePlayers," GitHub. Accessed: May 29, 2026. [Online]. Available: https://github.com/batram/UCH-EvenMorePlayers ↩