𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Theme

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.

Tip

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:

  1. The BepInEx 5 README's macOS guidance edits Steam launch options. Wrapping the binary inside the .app bundle is cleaner — Steam launches BepInEx through its normal flow with no Steam-side changes.
  2. On Apple Silicon, BepInEx 5.4.23.5's MonoMod.RuntimeDetour crashes during preload when patching arm64 Mono (DetourHelper.GetIdentifiable null deref inside HarmonyInteropFix). The wrapper forces the game under Rosetta (x86_64), where MonoMod works.
Tip

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.real exists but is not a Mach-O → stale shell-script wrapper from a previous bad run; remove it.
  • $BIN is 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.
  • $BIN is not a Mach-O and $BIN.real is a Mach-O → already wrapped; leave .real alone 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

  1. batram, "UCH-EvenMorePlayers," GitHub. Accessed: May 29, 2026. [Online]. Available: https://github.com/batram/UCH-EvenMorePlayers

Previous
PS5 Controller on Minecraft Java (macOS)