{ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", "meta": { "version": "PTDL_v2", "update_url": "https://raw.githubusercontent.com/Open2077/open77-server-egg/main/egg-open77.json" }, "exported_at": "2026-10-13T22:46:55+00:00", "name": "OPEN//77 (Cyberpunk 2077 multiplayer)", "author": "dop@dop42.xyz", "description": "OPEN//77 dedicated server (Cyberpunk 2077 multiplayer). Pulls the latest build from the official CDN (cdn.open77.dev) on every start, checks its SHA-256, and keeps config, identities and resource data. Stable or unstable channel, or a pinned version.", "features": null, "docker_images": { "Debian + .NET 10 (recommended)": "ghcr.io/ptero-eggs/yolks:dotnet_10", "Debian (ptero-eggs)": "ghcr.io/ptero-eggs/yolks:debian" }, "file_denylist": [], "startup": "bash ./open77-boot.sh", "config": { "files": "{}", "startup": "{\"done\": \"OPEN//77 server listening on UDP\"}", "logs": "{}", "stop": "^C" }, "scripts": { "installation": { "script": "#!/bin/bash\n# OPEN//77 dedicated server - Pterodactyl install / reinstall\nset -e\napt-get update -qq && apt-get install -y -qq curl jq ca-certificates tar coreutils >/dev/null\nmkdir -p /mnt/server && cd /mnt/server\n\ncat > open77-update.sh <<'OPEN77_EOF'\n#!/bin/bash\n# OPEN//77 updater for Pterodactyl: fetches the latest server build from the official CDN,\n# verifies its SHA-256 and lays it over the install without touching private state.\n#\n# Env:\n# OP77_CHANNEL stable (default) | unstable\n# OP77_VERSION latest (default) or an exact version, e.g. 2.31.21+op77.138 (pin)\n# AUTO_UPDATE 1 (default) / 0 — when 0, only installs if nothing is installed yet\n# OP77_FORCE 1 to reinstall even if the version already matches\n# SERVER_DIR install directory (default: current directory)\n# Needs curl, tar, sha256sum, sed; jq is optional (the runtime yolk has none).\nset -euo pipefail\n\nCDN=\"https://cdn.open77.dev/server\"\nCHANNEL=\"${OP77_CHANNEL:-stable}\"\nWANTED=\"${OP77_VERSION:-latest}\"\nDIR=\"${SERVER_DIR:-$PWD}\"\nMANIFEST=\"$DIR/package-manifest.json\"\n\nlog() { printf '\\033[1;36m[open77-egg]\\033[0m %s\\n' \"$*\"; }\ndie() { printf '\\033[1;31m[open77-egg]\\033[0m %s\\n' \"$*\" >&2; exit 1; }\n\n# The top-level \"version\" is the first one in both latest.json and package-manifest.json.\njson_version() { sed -n 's/.*\"version\": *\"\\([^\"]*\\)\".*/\\1/p' \"$1\" | head -n1; }\n\n# A field of the linux-x64 build in latest.json.\nbuild_field() { # $1 = latest.json, $2 = url|archiveSha256\n if command -v jq >/dev/null 2>&1; then\n jq -r --arg k \"$2\" '.builds[\"linux-x64\"][$k] // empty' \"$1\"\n else\n sed -n '/\"linux-x64\"/,/}/p' \"$1\" | sed -n \"s/.*\\\"$2\\\": *\\\"\\([^\\\"]*\\)\\\".*/\\1/p\" | head -n1\n fi\n}\n\n# Shipped file paths of a package-manifest.json, one per line (pretty-printed or minified).\nmanifest_paths() { grep -oE '\"path\": *\"[^\"]*\"' \"$1\" | cut -d'\"' -f4; }\n\n# \"sha256 path\" lines of a package-manifest.json, for sha256sum -c.\nmanifest_checklist() {\n grep -oE '\"(path|sha256)\": *\"[^\"]*\"' \"$1\" \\\n | awk -F'\"' '$2 == \"path\" { p = $4 } $2 == \"sha256\" && p != \"\" { print $4 \" \" p; p = \"\" }'\n}\n\nCURRENT=\"\"\n[[ -f \"$MANIFEST\" ]] && CURRENT=\"$(json_version \"$MANIFEST\")\"\nINSTALLED=0\n[[ -x \"$DIR/Open77.Server\" && -n \"$CURRENT\" ]] && INSTALLED=1\n\nif [[ \"${AUTO_UPDATE:-1}\" != \"1\" && $INSTALLED == 1 ]]; then\n log \"Auto-update disabled, keeping $CURRENT\"\n exit 0\nfi\n\n# Never /tmp: Wings mounts it as a small tmpfs (100 MB by default), the archive and its\n# extraction need about 300 MB. The server volume has the room.\nWORK=\"$DIR/.open77-egg/tmp\"\nrm -rf \"$WORK\" && mkdir -p \"$WORK\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nif [[ \"$WANTED\" == \"latest\" || -z \"$WANTED\" ]]; then\n case \"$CHANNEL\" in\n stable) MANIFEST_URL=\"$CDN/latest.json\" ;;\n unstable) MANIFEST_URL=\"$CDN/unstable/latest.json\" ;;\n *) die \"Unknown OP77_CHANNEL '$CHANNEL' (stable|unstable)\" ;;\n esac\n if ! curl -fsSL --retry 3 --max-time 30 \"$MANIFEST_URL\" -o \"$WORK/latest.json\"; then\n [[ $INSTALLED == 1 ]] && { log \"CDN unreachable, starting installed $CURRENT\"; exit 0; }\n die \"Cannot fetch $MANIFEST_URL\"\n fi\n VERSION=\"$(json_version \"$WORK/latest.json\")\"\n URL=\"$(build_field \"$WORK/latest.json\" url)\"\n SHA=\"$(build_field \"$WORK/latest.json\" archiveSha256)\"\n [[ -n \"$SHA\" ]] || die \"Manifest has no archive hash for linux-x64, refusing an unverified download\"\nelse\n VERSION=\"$WANTED\"\n URL=\"$CDN/$VERSION/open77-server-$VERSION-linux-x64.tar.gz\"\n SHA=\"\"\nfi\n[[ -n \"$VERSION\" && -n \"$URL\" ]] || die \"Manifest did not describe a linux-x64 build\"\n\nif [[ \"$VERSION\" == \"$CURRENT\" && $INSTALLED == 1 && \"${OP77_FORCE:-0}\" != \"1\" ]]; then\n # Runtime files outside resources/ are never edited by hand: a mismatch means a broken\n # install (interrupted copy, full disk), so it is reinstalled. Resources may be customised.\n if (cd \"$DIR\" && manifest_checklist package-manifest.json | grep -v ' resources/' \\\n | sha256sum -c --quiet --strict >/dev/null 2>&1); then\n log \"Up to date: $VERSION ($CHANNEL)\"\n exit 0\n fi\n log \"Installed runtime files are damaged, repairing $VERSION\"\nfi\n\nlog \"Installing OPEN//77 server $VERSION (installed: ${CURRENT:-none})\"\ncurl -fL --retry 3 --progress-bar \"$URL\" -o \"$WORK/server.tar.gz\" || die \"Download failed: $URL\"\n\nif [[ -n \"$SHA\" ]]; then\n echo \"$SHA $WORK/server.tar.gz\" | sha256sum -c --status || die \"Archive SHA-256 mismatch, nothing applied\"\n log \"Archive SHA-256 verified\"\nelse\n log \"Pinned version: no CDN archive hash, relying on the per-file hashes of package-manifest.json\"\nfi\n\nmkdir -p \"$WORK/stage\"\ntar -xzf \"$WORK/server.tar.gz\" -C \"$WORK/stage\" --no-same-owner\nrm -f \"$WORK/server.tar.gz\"\n[[ -x \"$WORK/stage/Open77.Server\" && -f \"$WORK/stage/package-manifest.json\" ]] \\\n || die \"Archive layout unexpected (Open77.Server or package-manifest.json missing)\"\n\n# Check every staged file before anything in the install is touched.\n(cd \"$WORK/stage\" && manifest_checklist package-manifest.json | sha256sum -c --quiet --strict) \\\n || die \"A file of the archive does not match package-manifest.json, nothing applied\"\n\n# The archive never ships these, but guard them anyway: they are the server's private state.\nfor f in server.jsonc acl.jsonc tunables.json server-icon.png .open77 .open77-egg; do\n rm -rf \"${WORK:?}/stage/$f\"\ndone\n\n# Files the previous release shipped and this one no longer does. Only those are removed:\n# Workshop resources, custom resources, resource data/ and logs were never in a manifest.\nif [[ -f \"$MANIFEST\" ]]; then\n manifest_paths \"$MANIFEST\" | sort -u > \"$WORK/old.txt\"\n manifest_paths \"$WORK/stage/package-manifest.json\" | sort -u > \"$WORK/new.txt\"\n removed=0\n while IFS= read -r p; do\n [[ -z \"$p\" || \"$p\" == /* || \"$p\" == *..* ]] && continue\n rm -f -- \"$DIR/$p\" && removed=$((removed + 1))\n done < <(comm -23 \"$WORK/old.txt\" \"$WORK/new.txt\")\n (( removed > 0 )) && log \"Removed $removed file(s) dropped by this release\"\nfi\n\ncp -a \"$WORK/stage/.\" \"$DIR/\"\nchmod +x \"$DIR/Open77.Server\" \"$DIR/start.sh\" 2>/dev/null || true\n\n(cd \"$DIR\" && manifest_checklist package-manifest.json | sha256sum -c --quiet --strict) \\\n || die \"Installed files do not match package-manifest.json (disk full?). Reinstall the server.\"\n\nlog \"OPEN//77 server $VERSION ready\"\nOPEN77_EOF\nchmod +x open77-update.sh\n\ncat > open77-boot.sh <<'OPEN77_EOF'\n#!/bin/bash\n# OPEN//77 Pterodactyl entrypoint: update, then start the server bound to the panel allocations.\ncd /home/container || exit 1\n\nlog() { printf '\\033[1;36m[open77-egg]\\033[0m %s\\n' \"$*\"; }\nwarn() { printf '\\033[1;33m[open77-egg]\\033[0m %s\\n' \"$*\"; }\n\nbash ./open77-update.sh || warn \"Update step failed, trying to start the installed build\"\n[[ -x ./Open77.Server ]] || { warn \"No server installed. Reinstall the server from the panel.\"; exit 1; }\n\nGAME_PORT=\"${SERVER_PORT:-11778}\"\nDL_PORT=\"${DOWNLOAD_PORT:-$((GAME_PORT + 1))}\"\nPUBLIC=\"${PUBLIC_IP:-}\"\n# Wings exposes the allocation IP; 0.0.0.0 or a private address cannot be advertised to players.\n[[ -z \"$PUBLIC\" && -n \"${SERVER_IP:-}\" && \"$SERVER_IP\" != \"0.0.0.0\" ]] && PUBLIC=\"$SERVER_IP\"\n\n# First start without a config: Warden onboarding wizard (PIN printed in this console).\nif [[ ! -f server.jsonc ]]; then\n if [[ -z \"${WARDEN_PORT:-}\" ]]; then\n warn \"No server.jsonc and no WARDEN_PORT: add a TCP allocation for the setup wizard,\"\n warn \"or set SETUP_MODE=headless and reinstall to generate the config automatically.\"\n exit 1\n fi\n log \"First start: open http://${PUBLIC:-}:${WARDEN_PORT}/ and enter the PIN below.\"\n log \"In the wizard use game port ${GAME_PORT}/UDP and download port ${DL_PORT}/TCP.\"\n export OP77_WARDEN_SETUP_LISTEN=\"http://0.0.0.0:${WARDEN_PORT}\"\n exec ./Open77.Server --config server.jsonc\nfi\n\n[[ -z \"${DOWNLOAD_PORT:-}\" ]] && warn \"DOWNLOAD_PORT empty, using ${DL_PORT}: that port must be one of this server's allocations.\"\n[[ -z \"$PUBLIC\" ]] && warn \"No public IP (set PUBLIC_IP): players outside this host cannot reach the game or downloads.\"\n\n# Customer-side settings first.\nARGS=(--no-setup --config server.jsonc --log-level \"${LOG_LEVEL:-info}\")\n[[ -n \"${SERVER_NAME:-}\" ]] && ARGS+=(--name \"$SERVER_NAME\")\n[[ -n \"${VISIBILITY:-}\" ]] && ARGS+=(--visibility \"$VISIBILITY\")\n[[ -n \"${OP77_DATABASE_CONNECTION:-}\" ]] && ARGS+=(--set \"database.enabled=true\")\n[[ \"${WORKSHOP:-1}\" == \"1\" ]] && ARGS+=(--set \"warden.hubFileGatewayOrigin=https://files.open2077.net\")\n\n# Free-form extra arguments from the panel, split on spaces, never glob-expanded.\nif [[ -n \"${EXTRA_ARGS:-}\" ]]; then\n read -r -a EXTRA <<< \"$EXTRA_ARGS\"\n ARGS+=(\"${EXTRA[@]}\")\nfi\n\n# Host-enforced settings last: on the command line the last value of a field wins, so\n# EXTRA_ARGS, server.jsonc or Warden can never move the ports, address or slot count.\nARGS+=(--port \"$GAME_PORT\")\nARGS+=(--set \"resources.download.enabled=true\" --set \"resources.download.listenUrl=http://0.0.0.0:${DL_PORT}/\")\n[[ -n \"$PUBLIC\" ]] && ARGS+=(--public-ip \"$PUBLIC\")\n# Resource URL given to players: an HTTPS reverse proxy / CDN when set, else plain HTTP on the allocation.\nif [[ -n \"${DOWNLOAD_PUBLIC_URL:-}\" ]]; then\n ARGS+=(--set \"resources.download.publicBaseUrl=${DOWNLOAD_PUBLIC_URL%/}/\")\nelif [[ -n \"$PUBLIC\" ]]; then\n ARGS+=(--set \"resources.download.publicBaseUrl=http://${PUBLIC}:${DL_PORT}/\")\nfi\n[[ -n \"${MAX_PLAYERS:-}\" ]] && ARGS+=(--max-players \"$MAX_PLAYERS\")\nif [[ -n \"${WARDEN_PORT:-}\" ]]; then\n ARGS+=(--set \"warden.enabled=true\" --set \"warden.listenUrl=http://0.0.0.0:${WARDEN_PORT}\")\nfi\n\nlog \"Starting: ./Open77.Server ${ARGS[*]}\"\nexec ./Open77.Server \"${ARGS[@]}\"\nOPEN77_EOF\nchmod +x open77-boot.sh\n\ncat > open77-genconfig.sh <<'OPEN77_EOF'\n#!/bin/bash\n# Writes a minimal server.jsonc for headless installs (SETUP_MODE=headless).\n# Ports, public IP and player count are overridden at every boot by open77-boot.sh.\nset -euo pipefail\nDIR=\"${SERVER_DIR:-$PWD}\"\ncd \"$DIR\"\n[[ -f server.jsonc ]] && { echo \"[open77-egg] server.jsonc exists, leaving it untouched\"; exit 0; }\n\nTEMPLATE=\"${OP77_TEMPLATE:-freeroam}\"\n[[ -f \"templates/$TEMPLATE/template.json\" ]] || { echo \"[open77-egg] Unknown template '$TEMPLATE'\" >&2; exit 1; }\n\n# Template lists resource names; package-manifest.json maps them to directories under resources/.\nLOAD=\"$(jq -c --slurpfile m package-manifest.json '[.resources[] | $m[0].resources[.] // .]' \"templates/$TEMPLATE/template.json\")\"\nTAGS=\"$(jq -c '.defaultTags // []' \"templates/$TEMPLATE/template.json\")\"\n\njq -n \\\n --arg name \"${SERVER_NAME:-OPEN//77 server}\" \\\n --arg desc \"${SERVER_DESCRIPTION:-Hosted with Pterodactyl}\" \\\n --arg vis \"${VISIBILITY:-Public}\" \\\n --argjson port \"${SERVER_PORT:-11778}\" \\\n --argjson max \"${MAX_PLAYERS:-32}\" \\\n --argjson load \"$LOAD\" \\\n --argjson tags \"$TAGS\" \\\n'{\n \"$schema\": \"./config/server.schema.json\",\n schemaVersion: 1,\n identity: { name: $name, description: $desc, locale: \"en\", tags: $tags, visibility: $vis },\n network: { port: $port, publicEndpoint: null, maximumPlayers: $max },\n simulation: { tickRate: 30, snapshotRate: 20, expectedGameBuild: 23100, helloTimeoutSeconds: 10 },\n resources: { enabled: true, root: \"resources\", load: $load, autoStart: true },\n masterServer: { enabled: true, url: \"https://master.open2077.net/\", licenseKeyEnvironmentVariable: \"OP77_LICENSE_KEY\" }\n}' > server.jsonc\n\necho \"[open77-egg] server.jsonc generated from template '$TEMPLATE'\"\nOPEN77_EOF\nchmod +x open77-genconfig.sh\n\n# Reinstall always pulls again; server.jsonc, acl.jsonc, .open77/ and resource data are kept.\nSERVER_DIR=/mnt/server OP77_FORCE=1 AUTO_UPDATE=1 bash ./open77-update.sh\n\nif [[ \"${SETUP_MODE:-wizard}\" == \"headless\" ]]; then\n SERVER_DIR=/mnt/server bash ./open77-genconfig.sh\nfi\necho \"[open77-egg] Install complete\"\n", "container": "ghcr.io/ptero-eggs/installers:debian", "entrypoint": "bash" } }, "variables": [ { "name": "License key", "description": "Platform license key (op77_live_...) from https://open2077.net/account/keys. Required for the server to appear on the master list.", "env_variable": "OP77_LICENSE_KEY", "default_value": "", "user_viewable": true, "user_editable": true, "rules": "nullable|string|max:256", "field_type": "text" }, { "name": "Update channel", "description": "stable = production builds, unstable = test builds (newer client required).", "env_variable": "OP77_CHANNEL", "default_value": "stable", "user_viewable": true, "user_editable": true, "rules": "required|string|in:stable,unstable", "field_type": "text" }, { "name": "Version", "description": "latest = always the newest build of the channel. Otherwise an exact version to pin, e.g. 2.31.21+op77.138.", "env_variable": "OP77_VERSION", "default_value": "latest", "user_viewable": true, "user_editable": true, "rules": "required|string|max:64", "field_type": "text" }, { "name": "Auto update", "description": "1 = check the CDN and update on every start. 0 = never touch the installed build.", "env_variable": "AUTO_UPDATE", "default_value": "1", "user_viewable": true, "user_editable": true, "rules": "required|boolean", "field_type": "text" }, { "name": "Setup mode", "description": "wizard = first start opens Warden onboarding (PIN in the console, needs Warden port). headless = install writes server.jsonc from the template (reinstall to apply).", "env_variable": "SETUP_MODE", "default_value": "wizard", "user_viewable": true, "user_editable": true, "rules": "required|string|in:wizard,headless", "field_type": "text" }, { "name": "Template (headless)", "description": "Gamemode template under templates/ used by headless setup.", "env_variable": "OP77_TEMPLATE", "default_value": "freeroam", "user_viewable": true, "user_editable": true, "rules": "required|string|max:64", "field_type": "text" }, { "name": "Public IP", "description": "Public IP players connect to. Empty = use the allocation IP (must not be 0.0.0.0). Set by the host.", "env_variable": "PUBLIC_IP", "default_value": "", "user_viewable": true, "user_editable": false, "rules": "nullable|ip", "field_type": "text" }, { "name": "Resource download port", "description": "TCP port of a second allocation for resource downloads. Empty = game port + 1 (must also be allocated). Set by the host.", "env_variable": "DOWNLOAD_PORT", "default_value": "", "user_viewable": true, "user_editable": false, "rules": "nullable|integer|between:1,65535", "field_type": "text" }, { "name": "Download public URL", "description": "Optional URL players download resources from, e.g. https://cdn.example.net/ behind a TLS reverse proxy pointing at the download port. Empty = http://PUBLIC_IP:DOWNLOAD_PORT/.", "env_variable": "DOWNLOAD_PUBLIC_URL", "default_value": "", "user_viewable": true, "user_editable": true, "rules": "nullable|url|max:512", "field_type": "text" }, { "name": "Warden port", "description": "TCP port of an extra allocation for the Warden web panel / setup wizard. Empty = disabled. Set by the host.", "env_variable": "WARDEN_PORT", "default_value": "", "user_viewable": true, "user_editable": false, "rules": "nullable|integer|between:1,65535", "field_type": "text" }, { "name": "Max players", "description": "Overrides network.maximumPlayers at every start. Empty = value from server.jsonc. Set by the host (slot count).", "env_variable": "MAX_PLAYERS", "default_value": "32", "user_viewable": true, "user_editable": false, "rules": "nullable|integer|between:1,4096", "field_type": "text" }, { "name": "Server name", "description": "Overrides identity.name at every start. Empty = value from server.jsonc / Warden.", "env_variable": "SERVER_NAME", "default_value": "", "user_viewable": true, "user_editable": true, "rules": "nullable|string|max:128", "field_type": "text" }, { "name": "Visibility", "description": "Public, Unlisted or Private. Empty = value from server.jsonc.", "env_variable": "VISIBILITY", "default_value": "", "user_viewable": true, "user_editable": true, "rules": "nullable|string|in:Public,Unlisted,Private", "field_type": "text" }, { "name": "Log level", "description": "Minimum console severity.", "env_variable": "LOG_LEVEL", "default_value": "info", "user_viewable": true, "user_editable": true, "rules": "required|string|in:trace,debug,info,warn,error", "field_type": "text" }, { "name": "Workshop installs", "description": "1 = allow installing Workshop creations from Warden (files.open2077.net).", "env_variable": "WORKSHOP", "default_value": "1", "user_viewable": true, "user_editable": true, "rules": "required|boolean", "field_type": "text" }, { "name": "Database connection", "description": "Optional MySQL/MariaDB connection string for persistence. Kept out of the command line.", "env_variable": "OP77_DATABASE_CONNECTION", "default_value": "", "user_viewable": true, "user_editable": true, "rules": "nullable|string|max:512", "field_type": "text" }, { "name": "Extra arguments", "description": "Appended to the command line, e.g. --set simulation.tickRate=60 --tick-rate 60.", "env_variable": "EXTRA_ARGS", "default_value": "", "user_viewable": true, "user_editable": true, "rules": "nullable|string|max:1024", "field_type": "text" } ] }