Project Zomboid B42 Active Users stuck at 0/32 - Working Linux workaround + AMP log rotation issue

Temporary Self-Fix / Workaround

If you are using Project Zomboid Build 42 on AMP/Linux and AMP always shows:

Active Users: 0 / 32

even though people are connected, this workaround restores AMP’s Active Users / @UserCount tracking.

Important: This is a temporary community workaround, not an official CubeCoders fix.

It has been tested on AMP 2.8.0.4, Debian Linux, Podman and Project Zomboid Build 42.

The installer performs compatibility checks before changing AMP and creates a timestamped backup of the AMP configuration.

Before you start

You need:

  • SSH/terminal access to the Linux machine running AMP
  • an account that can use sudo
  • your Project Zomboid AMP instance name

You can see the instance name in AMP.

For example:

ProjectZomboid01

Easy install

At the top of the command below is:

INSTANCE="ProjectZomboid01"

If your instance is called ProjectZomboid01, leave it alone.

If your instance has a different name, for example:

PZServer01

change only:

INSTANCE="ProjectZomboid01"

to:

INSTANCE="PZServer01"

Then copy the entire block below into your Linux terminal and press Enter.

The installer will:

  • locate the AMP instance;
  • check the Project Zomboid Build 42 logs exist;
  • create a timestamped backup of GenericModule.kvp;
  • change only the AMP settings required for player tracking;
  • create a stable connection log for AMP;
  • install a separate bridge service for this specific AMP instance;
  • verify the bridge is working;
  • restart only the Project Zomboid AMP instance;
  • automatically restore GenericModule.kvp if installation fails before completion.
sudo bash <<'PZB42FIX'
set -u

INSTANCE="ProjectZomboid01"

echo "=================================================="
echo " Project Zomboid B42 - AMP Active Users Workaround"
echo "=================================================="
echo
echo "Instance requested: $INSTANCE"
echo

# --------------------------------------------------
# Basic safety checks
# --------------------------------------------------

for CMD in \
    python3 systemctl journalctl find stat grep sed getent \
    date cp cut bash runuser mkdir touch chmod chown
do
    if ! command -v "$CMD" >/dev/null 2>&1; then
        echo "ERROR: Required command '$CMD' was not found."
        exit 1
    fi
done

SAFE_INSTANCE="$(printf '%s' "$INSTANCE" | sed 's/[^A-Za-z0-9_.-]/_/g')"

if [ -z "$SAFE_INSTANCE" ]; then
    echo "ERROR: Invalid AMP instance name."
    exit 1
fi

echo "1/9 - Finding AMP instance..."

mapfile -t CFG_MATCHES < <(
    find /home /opt /srv /root \
        -type f \
        -path "*/.ampdata/instances/$INSTANCE/GenericModule.kvp" \
        -print 2>/dev/null
)

if [ "${#CFG_MATCHES[@]}" -eq 0 ]; then
    echo
    echo "ERROR: Could not find GenericModule.kvp for:"
    echo "  $INSTANCE"
    echo
    echo "Check INSTANCE= at the top of the installer."
    echo "No AMP files have been changed."
    exit 1
fi

if [ "${#CFG_MATCHES[@]}" -gt 1 ]; then
    echo
    echo "ERROR: More than one matching AMP instance was found:"
    printf '  %s\n' "${CFG_MATCHES[@]}"
    echo
    echo "The installer will not guess which one to modify."
    echo "No AMP files have been changed."
    exit 1
fi

CFG="${CFG_MATCHES[0]}"
INSTANCE_DIR="$(dirname "$CFG")"

echo "Found:"
echo "  $INSTANCE_DIR"

# --------------------------------------------------
# Determine AMP owner and PZ application directory
# --------------------------------------------------

echo
echo "2/9 - Checking AMP configuration..."

AMP_USER="$(stat -c '%U' "$CFG")"
AMP_GROUP="$(stat -c '%G' "$CFG")"
CFG_MODE="$(stat -c '%a' "$CFG")"

if [ -z "$AMP_USER" ] || [ "$AMP_USER" = "UNKNOWN" ]; then
    echo "ERROR: Could not determine the AMP file owner."
    exit 1
fi

if ! getent passwd "$AMP_USER" >/dev/null; then
    echo "ERROR: AMP owner '$AMP_USER' is not a valid local user."
    exit 1
fi

APP_BASE_RAW="$(grep -m1 '^App.BaseDirectory=' "$CFG" | cut -d= -f2-)"

if [ -z "$APP_BASE_RAW" ]; then
    echo
    echo "ERROR: App.BaseDirectory was not found in:"
    echo "  $CFG"
    echo
    echo "No files have been changed."
    exit 1
fi

case "$APP_BASE_RAW" in
    /*)
        PZBASE="${APP_BASE_RAW%/}"
        ;;

    *'{{'*|*'}}'*|*'$('*|*'`'*)
        echo
        echo "ERROR: App.BaseDirectory contains an unsupported expression:"
        echo "  $APP_BASE_RAW"
        echo
        echo "The installer will not guess how to resolve it."
        echo "No files have been changed."
        exit 1
        ;;

    *)
        APP_BASE_RAW="${APP_BASE_RAW#./}"
        APP_BASE_RAW="${APP_BASE_RAW%/}"
        PZBASE="$INSTANCE_DIR/$APP_BASE_RAW"
        ;;
esac

LOGDIR="$PZBASE/Zomboid/Logs"
BRIDGEDIR="$PZBASE/amp-connections"
BRIDGEFILE="$BRIDGEDIR/current_connections.txt"
STATEFILE="$BRIDGEDIR/bridge-state.json"

SCRIPT="/usr/local/libexec/pz-amp-connection-bridge-$SAFE_INSTANCE.py"
SERVICE_NAME="pz-amp-connection-bridge-$SAFE_INSTANCE.service"
SERVICE="/etc/systemd/system/$SERVICE_NAME"

if [ ! -d "$PZBASE" ]; then
    echo
    echo "ERROR: Project Zomboid application directory does not exist:"
    echo "  $PZBASE"
    echo
    echo "No files have been changed."
    exit 1
fi

if [ ! -d "$LOGDIR" ]; then
    echo
    echo "ERROR: Project Zomboid Logs directory was not found:"
    echo "  $LOGDIR"
    echo
    echo "Start the Project Zomboid server at least once, then try again."
    echo "No files have been changed."
    exit 1
fi

echo "AMP user:        $AMP_USER"
echo "Instance folder: $INSTANCE_DIR"
echo "PZ folder:       $PZBASE"
echo "B42 logs:        $LOGDIR"

mapfile -t CONNECTION_LOGS < <(
    find "$LOGDIR" \
        -maxdepth 1 \
        -type f \
        -name '*_connections.txt' \
        -print 2>/dev/null
)

if [ "${#CONNECTION_LOGS[@]}" -eq 0 ]; then
    echo
    echo "WARNING: No Build 42 *_connections.txt logs were found."
    echo
    echo "Start Project Zomboid and allow it to reach Ready,"
    echo "then run this installer again."
    echo
    echo "No files have been changed."
    exit 1
fi

echo "Found ${#CONNECTION_LOGS[@]} Build 42 connection log(s)."

# --------------------------------------------------
# Refuse conflicting/previous bridge installations
# --------------------------------------------------

echo
echo "Checking for an existing workaround..."

if systemctl is-active --quiet "pz-amp-connection-bridge.service" 2>/dev/null \
   || [ -f "/etc/systemd/system/pz-amp-connection-bridge.service" ]; then
    echo
    echo "ERROR: An older PZ B42 bridge installation was detected:"
    echo "  pz-amp-connection-bridge.service"
    echo
    echo "Remove the older workaround before running this installer."
    echo "No AMP files have been changed."
    exit 1
fi

if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null \
   || [ -f "$SERVICE" ] \
   || [ -f "$SCRIPT" ]; then
    echo
    echo "ERROR: This workaround already appears to be installed for:"
    echo "  $INSTANCE"
    echo
    echo "Existing service:"
    echo "  $SERVICE_NAME"
    echo
    echo "The installer will not overwrite an existing installation."
    echo "No AMP files have been changed."
    exit 1
fi

# --------------------------------------------------
# Backup
# --------------------------------------------------

echo
echo "3/9 - Creating timestamped configuration backup..."

TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
BACKUP="$CFG.b42-active-users-backup-$TIMESTAMP"

cp -a -- "$CFG" "$BACKUP"

if [ ! -f "$BACKUP" ]; then
    echo "ERROR: Configuration backup could not be created."
    exit 1
fi

echo "Backup:"
echo "  $BACKUP"

INSTALL_OK=0
SERVICE_CREATED=0
SCRIPT_CREATED=0
BRIDGEDIR_CREATED=0
BRIDGEFILE_CREATED=0

if [ ! -d "$BRIDGEDIR" ]; then
    BRIDGEDIR_CREATED=1
fi

if [ ! -e "$BRIDGEFILE" ]; then
    BRIDGEFILE_CREATED=1
fi

rollback() {
    RESULT=$?

    if [ "$INSTALL_OK" -eq 0 ]; then
        echo
        echo "=================================================="
        echo " INSTALLATION FAILED - ROLLING BACK"
        echo "=================================================="

        if [ -f "$BACKUP" ]; then
            cp -a -- "$BACKUP" "$CFG"
            echo "Restored GenericModule.kvp."
        fi

        if [ "$SERVICE_CREATED" -eq 1 ]; then
            systemctl disable --now "$SERVICE_NAME" >/dev/null 2>&1 || true
            rm -f "$SERVICE"
            systemctl daemon-reload
            systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true
            echo "Removed new bridge service."
        fi

        if [ "$SCRIPT_CREATED" -eq 1 ]; then
            rm -f "$SCRIPT"
            echo "Removed new bridge script."
        fi

        if [ "$BRIDGEFILE_CREATED" -eq 1 ]; then
            rm -f "$BRIDGEFILE" "$STATEFILE"
        fi

        if [ "$BRIDGEDIR_CREATED" -eq 1 ]; then
            rmdir "$BRIDGEDIR" 2>/dev/null || true
        fi

        echo
        echo "Original AMP configuration restored."
    fi

    exit "$RESULT"
}

trap rollback EXIT

# --------------------------------------------------
# Modify only AMP player-tracking settings
# --------------------------------------------------

echo
echo "4/9 - Updating AMP player tracking..."

python3 - "$CFG" <<'PY'
from pathlib import Path
import os
import sys
import tempfile

path = Path(sys.argv[1])
original = path.read_text()

settings = {
    "App.AdminMethod":
        "TailLogFile",

    "App.TailLogFilePath":
        "{{$FullBaseDir}}amp-connections/current_connections.txt",

    "Console.UserJoinRegex":
        r'^\[.*\] event="fully-connected" message="".*steam-id="(?<userid>\d+)".*username="(?<username>[^"]+)".*$',

    "Console.UserLeaveRegex":
        r'^\[.*\] event="disconnect" message="receive-disconnect".*steam-id="(?<userid>\d+)".*username="(?<username>[^"]+)".*$',
}

lines = original.splitlines()

for key, value in settings.items():
    prefix = key + "="

    matches = [
        i for i, line in enumerate(lines)
        if line.startswith(prefix)
    ]

    if len(matches) > 1:
        raise RuntimeError(
            f"Refusing to modify {key}: "
            f"it occurs more than once in GenericModule.kvp"
        )

    if matches:
        lines[matches[0]] = prefix + value
    else:
        lines.append(prefix + value)

new_data = "\n".join(lines) + "\n"

fd, temp_name = tempfile.mkstemp(
    prefix=path.name + ".b42fix-",
    dir=str(path.parent)
)

try:
    with os.fdopen(fd, "w") as f:
        f.write(new_data)
        f.flush()
        os.fsync(f.fileno())

    os.replace(temp_name, path)

except Exception:
    try:
        os.unlink(temp_name)
    except FileNotFoundError:
        pass
    raise
PY

chown "$AMP_USER:$AMP_GROUP" "$CFG"
chmod "$CFG_MODE" "$CFG"

grep -Fqx \
'App.AdminMethod=TailLogFile' "$CFG" || {
    echo "ERROR: App.AdminMethod validation failed."
    exit 1
}

grep -Fqx \
'App.TailLogFilePath={{$FullBaseDir}}amp-connections/current_connections.txt' "$CFG" || {
    echo "ERROR: TailLogFilePath validation failed."
    exit 1
}

grep -Fqx \
'Console.UserJoinRegex=^\[.*\] event="fully-connected" message="".*steam-id="(?<userid>\d+)".*username="(?<username>[^"]+)".*$' "$CFG" || {
    echo "ERROR: Join regex validation failed."
    exit 1
}

grep -Fqx \
'Console.UserLeaveRegex=^\[.*\] event="disconnect" message="receive-disconnect".*steam-id="(?<userid>\d+)".*username="(?<username>[^"]+)".*$' "$CFG" || {
    echo "ERROR: Leave regex validation failed."
    exit 1
}

echo "AMP configuration validated."

# --------------------------------------------------
# Create stable connection log
# --------------------------------------------------

echo
echo "5/9 - Creating stable AMP connection log..."

mkdir -p "$BRIDGEDIR"
touch "$BRIDGEFILE"

chown "$AMP_USER:$AMP_GROUP" "$BRIDGEDIR"
chown "$AMP_USER:$AMP_GROUP" "$BRIDGEFILE"

# --------------------------------------------------
# Python bridge
# --------------------------------------------------

echo
echo "6/9 - Installing Build 42 connection bridge..."

mkdir -p /usr/local/libexec

cat > "$SCRIPT" <<PY
#!/usr/bin/env python3

import json
import logging
import os
import time
from pathlib import Path

LOGDIR = Path(${LOGDIR@Q})
OUTDIR = Path(${BRIDGEDIR@Q})

OUTFILE = OUTDIR / "current_connections.txt"
STATEFILE = OUTDIR / "bridge-state.json"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s"
)


def load_state():
    try:
        data = json.loads(STATEFILE.read_text())

        return {
            "source": str(data.get("source", "")),
            "offset": int(data.get("offset", 0))
        }

    except FileNotFoundError:
        return {"source": "", "offset": 0}

    except Exception as exc:
        logging.warning("Could not read state: %s", exc)
        return {"source": "", "offset": 0}


def save_state(source, offset):
    tmp = STATEFILE.with_suffix(".tmp")

    tmp.write_text(
        json.dumps({
            "source": str(source),
            "offset": int(offset)
        })
    )

    os.replace(tmp, STATEFILE)


OUTDIR.mkdir(parents=True, exist_ok=True)
OUTFILE.touch(exist_ok=True)

state = load_state()
current_source = state["source"]
offset = state["offset"]

logging.info("Watching %s", LOGDIR)
logging.info("Stable AMP log: %s", OUTFILE)

while True:
    try:
        files = [
            p for p in LOGDIR.glob("*_connections.txt")
            if p.is_file()
        ]

        if not files:
            time.sleep(1)
            continue

        newest = max(
            files,
            key=lambda p: p.stat().st_mtime_ns
        )

        newest_str = str(newest)

        if newest_str != current_source:
            logging.info(
                "Following connection log: %s",
                newest
            )

            current_source = newest_str
            offset = 0
            save_state(current_source, offset)

        size = newest.stat().st_size

        if size < offset:
            logging.info(
                "Connection log truncated; resetting offset"
            )
            offset = 0

        if size > offset:
            with newest.open("rb") as src:
                src.seek(offset)
                data = src.read()

            if data:
                with OUTFILE.open("ab") as dst:
                    dst.write(data)
                    dst.flush()
                    os.fsync(dst.fileno())

                offset += len(data)
                save_state(current_source, offset)

    except FileNotFoundError:
        # Expected briefly during PZ log rotation/restart.
        time.sleep(1)

    except PermissionError as exc:
        logging.error("Permission error: %s", exc)
        time.sleep(5)

    except Exception:
        logging.exception("Unexpected bridge error")
        time.sleep(5)

    time.sleep(0.5)
PY

chmod 755 "$SCRIPT"
chown root:root "$SCRIPT"
SCRIPT_CREATED=1

if ! python3 -m py_compile "$SCRIPT"; then
    echo "ERROR: Python bridge validation failed."
    exit 1
fi

echo "Bridge script validated."

# --------------------------------------------------
# systemd service
# --------------------------------------------------

echo
echo "7/9 - Installing automatic bridge service..."

cat > "$SERVICE" <<EOF
[Unit]
Description=Project Zomboid B42 AMP connection bridge ($INSTANCE)
After=local-fs.target

[Service]
Type=simple
User=$AMP_USER
Group=$AMP_GROUP
ExecStart=/usr/bin/python3 $SCRIPT
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF

SERVICE_CREATED=1

systemctl daemon-reload
systemctl enable --now "$SERVICE_NAME"

sleep 2

if ! systemctl is-active --quiet "$SERVICE_NAME"; then
    echo
    echo "ERROR: Bridge service failed to start."
    echo
    journalctl \
        -u "$SERVICE_NAME" \
        -n 30 \
        --no-pager
    exit 1
fi

echo "Bridge service is running."

# --------------------------------------------------
# Verify bridge
# --------------------------------------------------

echo
echo "8/9 - Verifying Build 42 bridge..."

sleep 2

if [ ! -f "$BRIDGEFILE" ]; then
    echo "ERROR: Stable connection file does not exist."
    exit 1
fi

if [ ! -f "$STATEFILE" ]; then
    echo
    echo "ERROR: Bridge is running but state file was not created."
    echo
    journalctl \
        -u "$SERVICE_NAME" \
        -n 30 \
        --no-pager
    exit 1
fi

SOURCE_LOG="$(
    python3 - "$STATEFILE" <<'PY'
import json
import sys

try:
    with open(sys.argv[1]) as f:
        print(json.load(f).get("source", ""))
except Exception:
    print("")
PY
)"

if [ -z "$SOURCE_LOG" ]; then
    echo "ERROR: Bridge did not select a connection log."
    exit 1
fi

if [ ! -f "$SOURCE_LOG" ]; then
    echo
    echo "ERROR: Selected connection log does not exist:"
    echo "  $SOURCE_LOG"
    exit 1
fi

echo "Bridge is following:"
echo "  $SOURCE_LOG"

echo
echo "Stable AMP log:"
echo "  $BRIDGEFILE"

# --------------------------------------------------
# Restart AMP instance
# --------------------------------------------------

echo
echo "9/9 - Restarting Project Zomboid AMP instance..."

AMP_HOME="$(getent passwd "$AMP_USER" | cut -d: -f6)"

if [ -z "$AMP_HOME" ] || [ ! -d "$AMP_HOME" ]; then
    echo
    echo "WARNING: Could not determine the AMP user's home."
    echo "The workaround itself has installed successfully."
    echo "Restart the Project Zomboid instance manually in AMP."

else
    if [ -x "$AMP_HOME/ampinstmgr" ]; then
        runuser -u "$AMP_USER" -- \
            bash -c "cd $(printf '%q' "$AMP_HOME") && ./ampinstmgr --RestartInstance $(printf '%q' "$INSTANCE")"

    elif command -v ampinstmgr >/dev/null 2>&1; then
        runuser -u "$AMP_USER" -- \
            bash -c "cd $(printf '%q' "$AMP_HOME") && ampinstmgr --RestartInstance $(printf '%q' "$INSTANCE")"

    else
        echo
        echo "WARNING: ampinstmgr could not be found."
        echo "The workaround itself has installed successfully."
        echo "Restart Project Zomboid manually from the AMP web interface."
    fi
fi

INSTALL_OK=1
trap - EXIT

echo
echo "=================================================="
echo " INSTALL COMPLETE"
echo "=================================================="
echo
echo "Instance:"
echo "  $INSTANCE"
echo
echo "Bridge service:"
echo "  $SERVICE_NAME"
echo
echo "Configuration backup:"
echo "  $BACKUP"
echo
echo "PZ connection log:"
echo "  $SOURCE_LOG"
echo
echo "Stable AMP connection log:"
echo "  $BRIDGEFILE"
echo
echo "Wait until Project Zomboid is Ready."
echo "Then fully connect one player and refresh AMP."
echo
echo "Active Users should change:"
echo "  0 / 32  ->  1 / 32"
echo
echo "Diagnostics:"
echo "  sudo journalctl -u $SERVICE_NAME -n 50 --no-pager"
echo

PZB42FIX

What should happen afterwards?

Wait until Project Zomboid has completely started again.

Then have one player fully join the server.

Open the Project Zomboid instance in AMP and look at:

Metrics and Status

The Active Users value should change from:

0 / 32

to:

1 / 32

If a second player joins it should become:

2 / 32

When a player disconnects, the number should decrease again.

If it still shows 0 / 32

For an instance called:

ProjectZomboid01

the helper service will be called:

pz-amp-connection-bridge-ProjectZomboid01.service

Check whether it is running with:

sudo systemctl status \
pz-amp-connection-bridge-ProjectZomboid01.service

You want to see:

active (running)

For more detailed diagnostics:

sudo journalctl \
-u pz-amp-connection-bridge-ProjectZomboid01.service \
-n 50 \
--no-pager

The log should show which Build 42 *_connections.txt file the helper is currently following.

Undo / Roll Back

The installer creates a new timestamped backup of GenericModule.kvp.

For example:

GenericModule.kvp.b42-active-users-backup-20260814-050312

The exact backup path is displayed when installation finishes.

If you later want to remove the workaround, first stop and disable its service.

For ProjectZomboid01:

sudo systemctl disable --now \
pz-amp-connection-bridge-ProjectZomboid01.service

Then restore the GenericModule.kvp backup that was printed when the workaround was installed.

Do not use the example backup filename above unless it is actually the backup created on your machine.

After restoring the backup, restart the Project Zomboid instance.

If an official AMP fix becomes available, I recommend removing this workaround and returning to the official configuration.

Important Notes

This is a temporary community workaround, not an official CubeCoders fix.

Tested with:

AMP 2.8.0.4
Debian GNU/Linux 13
Podman
Project Zomboid Build 42
GenericModule

The workaround counts a player only when Build 42 reports:

event="fully-connected"

It deliberately does not use:

client-connect

because Build 42 can emit client-connect before the player has actually completed the connection process.

A normal disconnect is detected from:

event="disconnect" message="receive-disconnect"

The installer also creates a separate bridge service for each AMP instance, so multiple Project Zomboid instances do not share the same bridge process.

AMP/template updates may overwrite GenericModule.kvp.

If Active Users stops working after an AMP update, first check whether the official AMP template has fixed the problem. If not, the workaround may need to be reapplied.


Why This Workaround Is Needed

Project Zomboid Build 42 no longer appears to expose player join/leave information in stdout in the format expected by the current AMP Project Zomboid configuration.

Project Zomboid itself still knows the correct players.

For example, running:

players

in the Project Zomboid console reports the connected players correctly, while AMP can remain at:

Active Users: 0 / 32

Build 42 writes useful connection events into timestamped files under:

Zomboid/Logs/YYYY-MM-DD_HH-MM_connections.txt

For example:

Zomboid/Logs/2026-08-14_02-20_connections.txt
Zomboid/Logs/2026-08-14_03-27_connections.txt

A successful connection contains:

event="fully-connected" ... steam-id="765..." ... username="Player Name"

A normal disconnect contains:

event="disconnect" message="receive-disconnect" ... steam-id="765..." ... username="Player Name"

I confirmed that the following AMP regexes correctly track those events:

Console.UserJoinRegex=^\[.*\] event="fully-connected" message="".*steam-id="(?<userid>\d+)".*username="(?<username>[^"]+)".*$

Console.UserLeaveRegex=^\[.*\] event="disconnect" message="receive-disconnect".*steam-id="(?<userid>\d+)".*username="(?<username>[^"]+)".*$

AMP Active Users also works correctly when these events are supplied through a stable TailLogFile using:

App.AdminMethod=TailLogFile
App.TailLogFilePath={{$FullBaseDir}}amp-connections/current_connections.txt

I tested this directly and confirmed that a matching fully-connected event changes AMP from:

0 / 32

to:

1 / 32

So the player event parsing itself works.

The remaining problem is the filename.


Root Problem

Project Zomboid Build 42 creates a new timestamped connections log when the server starts.

For example:

2026-08-14_02-20_connections.txt
2026-08-14_03-27_connections.txt

AMP currently uses a fixed:

App.TailLogFilePath

so it cannot directly follow whichever *_connections.txt file becomes the current log after a Project Zomboid restart.

The workaround above therefore does this:

Project Zomboid B42
        |
        v
Zomboid/Logs/
*_connections.txt
        |
        v
small bridge service
        |
        v
amp-connections/current_connections.txt
        |
        v
AMP TailLogFile
        |
        v
Active Users / @UserCount

The bridge automatically detects when Project Zomboid creates a newer *_connections.txt file and begins following it.


Suggested AMP Fix

Would it be possible for the Project Zomboid Build 42 template or GenericModule to natively support following the newest file matching a wildcard/glob such as:

Zomboid/Logs/*_connections.txt

and automatically switch to the new matching file when Project Zomboid rotates/creates its connection log?

That would allow the Build 42 events to be parsed directly using:

Console.UserJoinRegex=^\[.*\] event="fully-connected" message="".*steam-id="(?<userid>\d+)".*username="(?<username>[^"]+)".*$

Console.UserLeaveRegex=^\[.*\] event="disconnect" message="receive-disconnect".*steam-id="(?<userid>\d+)".*username="(?<username>[^"]+)".*$

and would remove the need for the external bridge workaround.

A native solution which obtains the current player list from Project Zomboid’s players command would potentially solve the same issue as well.


System Information

Field Value
Operating System Debian GNU/Linux 13 on x86_64
Product AMP ‘Proteus’ v2.8.0.4 (Mainline)
Virtualization Podman
Application Project Zomboid
Module GenericModule
Running in Container Yes
Current State Ready

Reproduction Steps

  1. Run a Project Zomboid Build 42 server using AMP 2.8.0.4 / GenericModule on Linux with Podman.

  2. Connect one or more players.

  3. Run:

players

in the Project Zomboid console.

  1. Confirm Project Zomboid reports the connected player(s), while AMP still displays:
Active Users: 0 / 32
  1. Check:
Zomboid/Logs/

and observe that Build 42 writes player connection information into a timestamped:

*_connections.txt

file.

  1. Confirm a completed player connection produces:
event="fully-connected"

including steam-id and username.

  1. Confirm a normal disconnect produces:
event="disconnect" message="receive-disconnect"
  1. Configure GenericModule to use TailLogFile with a stable file and the join/leave regexes shown above.

  2. Feed the same Build 42 connection events into the stable file.

  3. Confirm AMP Active Users updates correctly.

  4. Restart Project Zomboid.

  5. Observe that Build 42 creates a new timestamped *_connections.txt file.

  6. Because App.TailLogFilePath points to one fixed path, AMP cannot automatically switch to the newly-created Build 42 connection log without an external bridge or similar workaround.

Worked fantastically, but appears to have murdered my console =(

Rolled back, as I would rather have console than active players working - but the adminmethod is still forcing TailLogFile and struggling to find how / why

I found a workaround that fixes the B42 Active Users stuck at 0/x issue without switching AMP from STDIO to TailLogFile.

I specifically wanted to keep STDIO, because changing App.AdminMethod=TailLogFile fixes the player count but can break/cripple the interactive AMP console.

This solution keeps:

App.AdminMethod=STDIO
App.HasWriteableConsole=True
App.HasReadableConsole=True

and only replaces App.ExecutableLinux with a small wrapper.

The wrapper:

  1. starts the normal Project Zomboid Java process with exactly the same arguments/stdin/stdout,
  2. watches the current B42 *_connections.txt,
  3. detects fully-connected and receive-disconnect,
  4. emits synthetic old-style LOG : Network ... lines to stdout,
  5. lets AMP’s existing Console.UserJoinRegex / Console.UserLeaveRegex handle them normally.

So AMP keeps its working console and Active Users updates correctly.

Tested successfully with:

  • Project Zomboid Build 42
  • AMP 2.8.0.4
  • Linux / Docker AMP instance
  • Join: 0/32 -> 1/32
  • Disconnect: 1/32 -> 0/32
  • AMP console still fully functional

Installation

Replace ProjectZomboid01 below with your actual AMP instance name.

You can find it with:

sudo su -l 
amp ampinstmgr -t 
exit

Then run as root:

sudo bash -s -- ProjectZomboid01 <<'INSTALL'
set -Eeuo pipefail

INSTANCE="$1"

CFG="$(find /home /opt /srv /root \
    -type f \
    -path "*/.ampdata/instances/$INSTANCE/GenericModule.kvp" \
    -print -quit 2>/dev/null)"

if [[ -z "$CFG" || ! -f "$CFG" ]]; then
    echo "ERROR: Could not find GenericModule.kvp for $INSTANCE"
    exit 1
fi

INSTANCE_DIR="$(dirname "$CFG")"

APP_BASE="$(grep -m1 '^App.BaseDirectory=' "$CFG" | cut -d= -f2-)"
ORIGINAL_EXE="$(grep -m1 '^App.ExecutableLinux=' "$CFG" | cut -d= -f2-)"
ADMIN_METHOD="$(grep -m1 '^App.AdminMethod=' "$CFG" | cut -d= -f2-)"

if [[ "$ADMIN_METHOD" != "STDIO" ]]; then
    echo "ERROR: App.AdminMethod is not STDIO."
    exit 1
fi

if [[ "$APP_BASE" = /* ]]; then
    PZBASE="${APP_BASE%/}"
else
    APP_BASE="${APP_BASE#./}"
    PZBASE="$INSTANCE_DIR/${APP_BASE%/}"
fi

JAVA="$PZBASE/jre64/bin/java"
LOGDIR="$PZBASE/Zomboid/Logs"

if [[ ! -x "$JAVA" ]]; then
    echo "ERROR: Java executable not found:"
    echo "$JAVA"
    exit 1
fi

if [[ ! -d "$LOGDIR" ]]; then
    echo "ERROR: Zomboid Logs directory not found:"
    echo "$LOGDIR"
    exit 1
fi

if [[ "$ORIGINAL_EXE" != */jre64/bin/java ]]; then
    echo "ERROR: Unexpected App.ExecutableLinux:"
    echo "$ORIGINAL_EXE"
    exit 1
fi

PREFIX="${ORIGINAL_EXE%/jre64/bin/java}"

if [[ -n "$PREFIX" ]]; then
    WRAPPER_REL="$PREFIX/amp-b42-stdio-wrapper.sh"
else
    WRAPPER_REL="amp-b42-stdio-wrapper.sh"
fi

WRAPPER="$PZBASE/amp-b42-stdio-wrapper.sh"

if [[ -e "$WRAPPER" ]]; then
    echo "ERROR: Wrapper already exists:"
    echo "$WRAPPER"
    exit 1
fi

# --------------------------------------------------
# Backup
# --------------------------------------------------

BACKUP="$CFG.b42-stdio-backup-$(date +%Y%m%d-%H%M%S)"
cp -a "$CFG" "$BACKUP"

echo "Backup created:"
echo "$BACKUP"

# --------------------------------------------------
# Wrapper
# --------------------------------------------------

cat > "$WRAPPER" <<'WRAPPER'
#!/bin/bash
set -u

BASE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
JAVA="$BASE/jre64/bin/java"
LOGDIR="$BASE/Zomboid/Logs"

emit_event() {
    local line="$1"
    local steam=""
    local username=""

    if [[ "$line" =~ steam-id=\"([0-9]+)\" ]]; then
        steam="${BASH_REMATCH[1]}"
    else
        return
    fi

    if [[ "$line" =~ username=\"([^\"]*)\" ]]; then
        username="${BASH_REMATCH[1]}"
    else
        return
    fi

    if [[ "$line" == *'event="fully-connected" message=""'* ]]; then

        printf 'LOG : Network , 0> 0> ConnectionManager: [fully-connected] bridge steam-id=%s bridge username="%s" bridge\n' \
            "$steam" "$username"

    elif [[ "$line" == *'event="disconnect" message="receive-disconnect"'* ]]; then

        printf 'LOG : Network , 0> 0> Disconnected player "%s" %s\n' \
            "$username" "$steam"
    fi
}

latest_connection_log() {
    find "$LOGDIR" \
        -maxdepth 1 \
        -type f \
        -name '*_connections.txt' \
        -printf '%T@ %p\n' 2>/dev/null |
        sort -nr |
        head -n1 |
        cut -d' ' -f2-
}

bridge_connections() {
    local parent_pid="$1"
    local current=""
    local newest=""
    local seen=0
    local count=0
    local first=0
    local line=""

    trap 'exit 0' TERM INT

    # Do not replay historical connections when AMP/PZ starts.
    current="$(latest_connection_log || true)"

    if [[ -n "$current" && -f "$current" ]]; then
        seen="$(wc -l < "$current")"
    fi

    while kill -0 "$parent_pid" 2>/dev/null; do

        newest="$(latest_connection_log || true)"

        if [[ -n "$newest" && -f "$newest" ]]; then

            # B42 creates timestamped connection log files.
            if [[ "$newest" != "$current" ]]; then
                current="$newest"
                seen=0
            fi

            count="$(wc -l < "$current")"

            if (( count < seen )); then
                seen=0
            fi

            if (( count > seen )); then
                first=$((seen + 1))

                while IFS= read -r line; do
                    emit_event "$line"
                done < <(sed -n "${first},${count}p" "$current")

                seen="$count"
            fi
        fi

        sleep 0.5
    done
}

if [[ ! -x "$JAVA" ]]; then
    echo "ERROR: Project Zomboid Java not found: $JAVA" >&2
    exit 127
fi

# Run the B42 connection log bridge in the background.
bridge_connections "$$" &

# Replace this shell with the real Java process.
#
# AMP therefore retains the same stdin/stdout/stderr and the interactive
# STDIO console continues to work normally.
exec "$JAVA" "$@"
WRAPPER

chmod 755 "$WRAPPER"
chown "$(stat -c '%U:%G' "$CFG")" "$WRAPPER"

bash -n "$WRAPPER"

# --------------------------------------------------
# Change only App.ExecutableLinux
# --------------------------------------------------

python3 - "$CFG" "$ORIGINAL_EXE" "$WRAPPER_REL" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
old = f"App.ExecutableLinux={sys.argv[2]}"
new = f"App.ExecutableLinux={sys.argv[3]}"

data = path.read_text()

if data.count(old) != 1:
    raise RuntimeError(
        f"Expected exactly one occurrence of {old!r}"
    )

path.write_text(data.replace(old, new, 1))
PY

echo
echo "Installed successfully."
echo
echo "AMP config:"
grep -E '^(App.ExecutableLinux|App.AdminMethod|App.HasReadableConsole|App.HasWriteableConsole)=' "$CFG"

echo
echo "Backup:"
echo "$BACKUP"

echo
echo "Restart this AMP instance for the change to take effect:"
echo "su -l amp -c 'ampinstmgr -r $INSTANCE'"
INSTALL

Then restart only the affected instance:

su -l amp -c 'ampinstmgr -r ProjectZomboid01'

After the restart, verify that the AMP console still works, e.g.:

players

Then connect a player.

A B42 connection entry such as:

event="fully-connected" ... steam-id="7656119..." ... username="Player"

is converted internally into something AMP’s existing regex already understands:

LOG : Network , 0> 0> ConnectionManager: [fully-connected] bridge steam-id=7656119... bridge username="Player" bridge

Disconnect works the same way using:

event="disconnect" message="receive-disconnect"

Rollback / restore backup

The installer prints the exact backup filename, for example:

/home/amp/.ampdata/instances/ProjectZomboid01/GenericModule.kvp.b42-stdio-backup-20260818-200630

To completely revert the workaround:

  1. Stop the Project Zomboid instance in AMP.
  2. Restore that backup over GenericModule.kvp.
  3. Delete amp-b42-stdio-wrapper.sh.
  4. Start the instance again.

Example:

CFG="/home/amp/.ampdata/instances/ProjectZomboid01/GenericModule.kvp"
BACKUP="/home/amp/.ampdata/instances/ProjectZomboid01/GenericModule.kvp.b42-stdio-backup-YYYYMMDD-HHMMSS"

cp -a "$BACKUP" "$CFG"

rm -f "/home/amp/.ampdata/instances/ProjectZomboid01/project-zomboid/380870/amp-b42-stdio-wrapper.sh"

Then start/restart the instance normally.

Only App.ExecutableLinux is changed by this workaround. App.AdminMethod=STDIO, the existing AMP console settings and the existing AMP player regexes remain untouched.

This is obviously still a workaround rather than an AMP-side fix, but so far it has behaved much better for me than changing the whole Generic module to TailLogFile.

Hey guys!

I have the same problem running my Zomboid server, and already applied the fix one time, however it always gets overwritten (the .kvp file) when I update my server (which is a scheduled task everyday) How do you solve that issue?
I have some mods subscribed that are frequently updated and players can not join if the server version is out of date, so I am restarting / updating my server quite frequently.

Help appreciated.