the files
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
# ============================================================
|
||||
# Build + package the 2025 Rec Room server (E12354 base, retargeted at
|
||||
# gameauth.ddns.net + our Photon IDs) into campus-deploy.tar.gz.
|
||||
#
|
||||
# This is the SUCCESSOR to Campus2025Server\PackageServer.ps1 (which
|
||||
# built the 2023 server). Uses the same tarball name so the existing
|
||||
# DeployCampusServer.bat / GoLive.bat upload pipeline works unchanged;
|
||||
# just point them at this folder.
|
||||
# ============================================================
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
$src = "Server\2025-RecRoom-Server-E12354-main" # C# project folder inside this repo
|
||||
$csproj = "$src\CampusServer.csproj"
|
||||
$out = Join-Path $env:TEMP "campus-deploy-2025"
|
||||
$pkg = "campus-deploy.tar.gz"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " ==========================================="
|
||||
Write-Host " Campus 2025 (E12354 base) - build + package"
|
||||
Write-Host " ==========================================="
|
||||
Write-Host ""
|
||||
|
||||
if (-not (Test-Path $csproj)) { Write-Host " [X] $csproj not found."; pause; exit 1 }
|
||||
|
||||
# Unique stamp for this package so DeployCampusServer.bat can confirm the VPS is
|
||||
# actually running THIS package (not some stale prior build).
|
||||
$tag = -join ((1..6) | ForEach-Object { "0123456789abcdef"[(Get-Random -Max 16)] })
|
||||
$buildId = "{0} #{1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $tag
|
||||
|
||||
Write-Host " [1/4] Publishing (linux-x64, self-contained) ..."
|
||||
if (Test-Path $out) { Remove-Item $out -Recurse -Force }
|
||||
dotnet publish $csproj -c Release -r linux-x64 --self-contained true -o $out -v quiet -clp:ErrorsOnly
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host " [X] Build failed - see errors above."; pause; exit 1 }
|
||||
|
||||
Write-Host " [2/4] Adding data + web + build stamp ..."
|
||||
# The 2025 server reads Data / Web from ContentRoot (Environment.CurrentDirectory on
|
||||
# the VPS = /opt/campus). Publish doesn't automatically copy either, so we do it here.
|
||||
# "Website" is the public site Kestrel now serves at the domain root (it replaced the
|
||||
# separate Flask process in Website/server.py). Without it in the tarball the landing
|
||||
# page 404s on the VPS even though it works locally.
|
||||
foreach ($d in @("Data", "Web", "AdminPanel", "Website")) {
|
||||
if (Test-Path "$src\$d") {
|
||||
# Data goes via robocopy so the avatar-pack dumps (Data\CDN\AvatarBlobs\<n>\ - tens of GB,
|
||||
# uploaded to the VPS separately and imported there) and any archives never enter the
|
||||
# tarball. Only the flat .assetbundle files in AvatarBlobs are served.
|
||||
if ($d -eq "Data") {
|
||||
$xd = @(); Get-ChildItem "$src\Data\CDN\AvatarBlobs" -Directory -ErrorAction SilentlyContinue | ForEach-Object { $xd += $_.FullName }
|
||||
$xd += "$src\Data\AvatarImportStaging"
|
||||
# Runtime-only state that must never be shipped over the live copy: the request log
|
||||
# (it would replace the VPS log with the dev box's) and the upload-guard quarantine.
|
||||
$xd += "$src\Data\Quarantine"
|
||||
$rc = & robocopy "$src\Data" "$out\Data" /E /XD @xd /XF *.zip *.7z *.rar APILog.txt /NFL /NDL /NJH /NJS /NP
|
||||
if ($LASTEXITCODE -ge 8) { throw "robocopy Data failed ($LASTEXITCODE)" }
|
||||
$n = (Get-ChildItem "$out\Data" -Recurse -File -ErrorAction SilentlyContinue).Count
|
||||
Write-Host (" + Data ({0} files, packs excluded)" -f $n)
|
||||
continue
|
||||
}
|
||||
Copy-Item "$src\$d" "$out\" -Recurse -Force
|
||||
$n = (Get-ChildItem "$src\$d" -Recurse -File -ErrorAction SilentlyContinue).Count
|
||||
Write-Host (" + $d ({0} files)" -f $n)
|
||||
}
|
||||
}
|
||||
|
||||
# Cache-bust the website's own scripts/styles with THIS build's tag. The HTML referenced
|
||||
# app.js?v=<hand-typed date> and Kestrel serves static files with max-age=3600, so after a
|
||||
# deploy every browser kept running the previous app.js for up to an hour (or until the
|
||||
# hand-typed tag was remembered and bumped). Now every packaged HTML page points at
|
||||
# app.js?v=<build tag>, which changes on every package, so the new site is live on the
|
||||
# next page load with no hard refresh.
|
||||
$siteOut = Join-Path $out "Website"
|
||||
if (Test-Path $siteOut) {
|
||||
$bumped = 0
|
||||
Get-ChildItem $siteOut -Filter *.html -Recurse | ForEach-Object {
|
||||
$html = [IO.File]::ReadAllText($_.FullName)
|
||||
$new = [regex]::Replace($html, '((?:app|data)\.js|style\.css)\?v=[A-Za-z0-9._-]+', ('$1?v=' + $tag))
|
||||
if ($new -ne $html) { [IO.File]::WriteAllText($_.FullName, $new, [Text.UTF8Encoding]::new($false)); $bumped++ }
|
||||
}
|
||||
Write-Host (" + cache-busted app.js/data.js/style.css in {0} page(s) (v={1})" -f $bumped, $tag)
|
||||
}
|
||||
|
||||
# Tachyon voice server SOURCE (C++23). It lives next to Server\ / Website\ in this repo
|
||||
# (tachyon-server-master\tachyon-server-master, double-nested from the zip). It can't be
|
||||
# cross-built from Windows, so the SOURCE ships in the tarball under tachyon/ and install.sh
|
||||
# builds it on the VPS - guarded and non-fatal, so a Tachyon problem never blocks the campus
|
||||
# server. Skip build/ (local output) and the empty submodule checkout; install.sh re-clones it.
|
||||
$tachyonSrc = "tachyon-server-master\tachyon-server-master"
|
||||
if (Test-Path "$tachyonSrc\CMakeLists.txt") {
|
||||
$tdst = "$out\tachyon"
|
||||
New-Item -ItemType Directory -Force -Path $tdst | Out-Null
|
||||
Get-ChildItem $tachyonSrc -Force | Where-Object { $_.Name -notin @('build', '.git', '.gitignore') } | ForEach-Object {
|
||||
Copy-Item $_.FullName $tdst -Recurse -Force
|
||||
}
|
||||
$tn = (Get-ChildItem $tdst -Recurse -File -ErrorAction SilentlyContinue).Count
|
||||
Write-Host (" + tachyon ({0} files)" -f $tn)
|
||||
}
|
||||
else {
|
||||
Write-Host " [!] tachyon source not found at $tachyonSrc - voice server NOT bundled"
|
||||
}
|
||||
# Build stamp - written verbatim, no trailing newline; the installer compares this
|
||||
# byte-for-byte against LastBuild.txt to prove the deploy landed.
|
||||
[IO.File]::WriteAllText("$out\Data\BUILD.txt", $buildId)
|
||||
[IO.File]::WriteAllText("LastBuild.txt", $buildId)
|
||||
|
||||
Write-Host " [3/4] Writing installer ..."
|
||||
# MUST be LF-only. A CRLF shebang makes bash fail with "bad interpreter".
|
||||
$install = @'
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
APP=/opt/campus
|
||||
UNIT=/etc/systemd/system/campus.service
|
||||
BIN_NAME=CampusServer
|
||||
|
||||
echo ""
|
||||
# One deploy at a time. Two overlapping deploys (a second DeployCampusServer.bat started while
|
||||
# the first was still in its Tachyon build step) replaced /opt/campus/*.dll under a RUNNING
|
||||
# server: .NET memory-maps its DLLs, so the process died with SIGBUS / BadImageFormatException
|
||||
# ("The format of the file '/opt/campus/CampusServer.dll' is invalid") - four times on
|
||||
# 2026-09-11, each within seconds of an SSH login. Every such crash drops every player at once.
|
||||
LOCK=/run/campus-deploy.lock
|
||||
exec 9>"$LOCK"
|
||||
if ! flock -n 9; then
|
||||
echo " [X] another deploy is still running (lock $LOCK). Wait for it to finish, then re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " [1/6] stopping the server ..."
|
||||
systemctl stop campus 2>/dev/null || true
|
||||
# stop is synchronous, but be certain nothing is still mapping the old binaries before they
|
||||
# are overwritten: wait for the unit to be fully inactive and the process to be gone.
|
||||
for i in $(seq 1 30); do
|
||||
if ! systemctl is-active --quiet campus && ! pgrep -x "$BIN_NAME" >/dev/null 2>&1; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
if pgrep -x "$BIN_NAME" >/dev/null 2>&1; then
|
||||
echo " [X] $BIN_NAME is still running after 30s - refusing to overwrite its files."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " [2/6] copying files ..."
|
||||
mkdir -p "$APP"
|
||||
|
||||
# Preserve live DBs across deploys — burner accounts, dorm state, heartbeats etc.
|
||||
# live in Data/DBs/*.db and would otherwise be clobbered by the tarball's seed copies.
|
||||
DB_BACKUP=$(mktemp -d /tmp/campus-dbs-XXXXXX)
|
||||
if [ -d "$APP/Data/DBs" ] && [ "$(ls -A "$APP/Data/DBs" 2>/dev/null)" ]; then
|
||||
echo " preserving existing $APP/Data/DBs -> $DB_BACKUP"
|
||||
cp -a "$APP/Data/DBs/." "$DB_BACKUP/"
|
||||
HAVE_LIVE_DBS=1
|
||||
else
|
||||
HAVE_LIVE_DBS=0
|
||||
fi
|
||||
|
||||
cp -r ./* "$APP"/
|
||||
chmod +x "$APP/$BIN_NAME" 2>/dev/null || true
|
||||
|
||||
if [ "$HAVE_LIVE_DBS" = "1" ]; then
|
||||
mkdir -p "$APP/Data/DBs"
|
||||
cp -a "$DB_BACKUP/." "$APP/Data/DBs/"
|
||||
echo " restored live DBs (kept burner accounts, etc.)"
|
||||
fi
|
||||
rm -rf "$DB_BACKUP"
|
||||
|
||||
echo " [3/6] writing / refreshing systemd unit ..."
|
||||
# Same unit name (campus.service) as the 2023 server so nothing else on the VPS
|
||||
# needs to change - Caddy, journalctl -u campus, our watch script all keep working.
|
||||
# ExecStart points at the 2025 binary (CampusServer, forced via AssemblyName in the csproj).
|
||||
cat > "$UNIT" <<UNITEOF
|
||||
[Unit]
|
||||
Description=Campus 2025 (E12354 base) Rec Room server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$APP
|
||||
# Kestrel binds inside the container. Caddy on the same box handles TLS + proxies
|
||||
# https://gameauth.ddns.net/* to localhost:8080, so only HTTP-on-8080 is needed.
|
||||
Environment=ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||
# Workstation GC (matches <ServerGarbageCollector>false</ServerGarbageCollector> in the csproj;
|
||||
# this env var would otherwise override it). Server GC reserved a per-core heap and held ~1.3 GB
|
||||
# resident for a few dozen players on a 4-core box.
|
||||
Environment=DOTNET_gcServer=0
|
||||
ExecStart=$APP/$BIN_NAME
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNITEOF
|
||||
systemctl daemon-reload
|
||||
|
||||
echo " [4/6] starting the server ..."
|
||||
systemctl enable campus >/dev/null 2>&1 || true
|
||||
systemctl start campus
|
||||
sleep 3
|
||||
|
||||
echo " [5/6] status ..."
|
||||
systemctl is-active campus || true
|
||||
|
||||
echo " [6/6] smoke test - GET / ..."
|
||||
# The 2025 server routes "/" to the NS response (WebController.NsRoot). If the
|
||||
# server is up and answering right, this prints 200. On any other code it dumps
|
||||
# the tail of the journal so the operator can see WHY it's not answering.
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://127.0.0.1:8080/ || echo 000)
|
||||
echo " GET http://127.0.0.1:8080/ -> HTTP $code"
|
||||
|
||||
# Same URL, but asking as a browser would: this must come back as the website, not the
|
||||
# NS JSON. Catches a missing Website/ folder, which is otherwise invisible until someone
|
||||
# opens the domain in a browser and gets the NameServer blob.
|
||||
site=$(curl -s --max-time 5 -H 'Accept: text/html' http://127.0.0.1:8080/ || true)
|
||||
if echo "$site" | grep -qi '<html'; then
|
||||
echo " GET / (Accept: text/html) -> website OK"
|
||||
else
|
||||
echo " [!] / with Accept: text/html did NOT return the website - is Website/ in the tarball?"
|
||||
fi
|
||||
|
||||
if [ "$code" != "200" ]; then
|
||||
echo ""
|
||||
echo " [!] Non-200 - last 20 lines of journal:"
|
||||
journalctl -u campus --no-pager -n 20 || true
|
||||
fi
|
||||
|
||||
# ---- Tachyon voice server (optional, NON-FATAL) --------------------------------------------
|
||||
# The campus server is already up above. This builds + runs the bundled Tachyon voice server
|
||||
# IF the C++ toolchain is present; ANY failure here only warns and the deploy still succeeds.
|
||||
# Voice stays on Photon until ConfigCampus.TachyonVoiceEndpoint is set, so this being down
|
||||
# changes nothing about the game until you flip that on.
|
||||
TACHYON="$APP/tachyon"
|
||||
# The address the CLIENT dials for voice (UDP). Must be reachable from the internet AND a raw
|
||||
# IP:port - the client's LiteNetLib voice connector does not resolve hostnames (every address in
|
||||
# Tachyon's own examples is an IP), so a DDNS hostname here means the client never dials it.
|
||||
# Written into a campus systemd drop-in below ONLY if Tachyon comes up, so the game is pointed at
|
||||
# Tachyon only when it's actually running.
|
||||
TACHYON_PUBLIC="50.114.179.17:7777"
|
||||
if [ -d "$TACHYON" ]; then
|
||||
echo ""
|
||||
echo " [tachyon] setting up voice server (non-fatal) ..."
|
||||
(
|
||||
set -e
|
||||
cd "$TACHYON"
|
||||
|
||||
# Deps (Debian/Ubuntu). build-essential is REQUIRED - it brings gcc, g++ AND make; installing
|
||||
# g++ alone leaves cmake unable to find "make" (CMAKE_MAKE_PROGRAM not set), which is exactly
|
||||
# what failed the first deploy. Errors are shown, not swallowed, so a broken apt is visible.
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
echo " installing build deps (build-essential, cmake, curl/ssl dev) ..."
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y || echo " [tachyon] apt-get update failed - continuing"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
build-essential cmake git pkg-config libcurl4-openssl-dev libssl-dev \
|
||||
|| echo " [tachyon] apt-get install failed - toolchain may be incomplete"
|
||||
fi
|
||||
|
||||
# Preflight: without a compiler AND make, cmake can't build. Skip with a clear, actionable
|
||||
# message rather than dumping raw CMake errors into the deploy log.
|
||||
if ! command -v cmake >/dev/null 2>&1 || ! command -v make >/dev/null 2>&1 \
|
||||
|| ! { command -v g++ >/dev/null 2>&1 || command -v c++ >/dev/null 2>&1; }; then
|
||||
echo " [tachyon] build toolchain missing (need cmake + make + g++). Install with:"
|
||||
echo " apt-get install -y build-essential cmake git pkg-config libcurl4-openssl-dev libssl-dev"
|
||||
echo " then re-run the deploy, or build by hand (tachyon/deploy/DEPLOY.md). Skipping - campus server is unaffected."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Submodule ships empty (LiteNetLibPP) - clone it if the source isn't there.
|
||||
if [ ! -e vendor/LiteNetLibPP/CMakeLists.txt ]; then
|
||||
echo " fetching LiteNetLibPP submodule ..."
|
||||
rm -rf vendor/LiteNetLibPP
|
||||
git clone --depth 1 https://github.com/Revan600/LiteNetLibPP.git vendor/LiteNetLibPP
|
||||
fi
|
||||
|
||||
# Build only when needed (first deploy, or a source file newer than the binary).
|
||||
if [ ! -x build/tachyon ] || [ -n "$(find src CMakeLists.txt -newer build/tachyon 2>/dev/null)" ]; then
|
||||
echo " building tachyon (this can take a couple of minutes) ..."
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release >/dev/null
|
||||
cmake --build build -j"$(nproc)" >/dev/null
|
||||
fi
|
||||
test -x build/tachyon
|
||||
|
||||
# Open UDP 7777 if ufw is the firewall (guarded - other setups are the operator's job).
|
||||
if command -v ufw >/dev/null 2>&1; then ufw allow 7777/udp >/dev/null 2>&1 || true; fi
|
||||
|
||||
# systemd unit pointing at the shipped build + conf.
|
||||
cat > /etc/systemd/system/tachyon.service <<TUNIT
|
||||
[Unit]
|
||||
Description=Tachyon voice server (Campus)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$TACHYON
|
||||
ExecStart=$TACHYON/build/tachyon --config $TACHYON/tachyon.conf
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
TUNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable tachyon >/dev/null 2>&1 || true
|
||||
systemctl restart tachyon
|
||||
sleep 1
|
||||
if systemctl is-active --quiet tachyon; then
|
||||
echo " tachyon is UP (UDP 7777)."
|
||||
else
|
||||
echo " [tachyon] built but not active - journalctl -u tachyon"
|
||||
fi
|
||||
) || echo " [tachyon] setup skipped/failed (toolchain missing?) - build manually: tachyon/deploy/DEPLOY.md. Campus server is unaffected."
|
||||
|
||||
# Point campus voice at Tachyon ONLY if it actually came up; otherwise make sure voice is on
|
||||
# Photon. Done via a systemd drop-in that sets CAMPUS_TACHYON_ENDPOINT (ConfigCampus reads it),
|
||||
# so no rebuild is needed to toggle. Keyed on the live service state, so it self-heals across
|
||||
# deploys: a Tachyon that stops building/running reverts the game to Photon on the next deploy.
|
||||
CAMPUS_DROPIN=/etc/systemd/system/campus.service.d/tachyon.conf
|
||||
if systemctl is-active --quiet tachyon 2>/dev/null; then
|
||||
mkdir -p /etc/systemd/system/campus.service.d
|
||||
cat > "$CAMPUS_DROPIN" <<DROPIN
|
||||
[Service]
|
||||
Environment=CAMPUS_TACHYON_ENDPOINT=$TACHYON_PUBLIC
|
||||
Environment=CAMPUS_TACHYON_SERVER_ID=tachyon-1
|
||||
DROPIN
|
||||
echo " [tachyon] voice ENABLED -> $TACHYON_PUBLIC (restarting campus to pick it up)"
|
||||
systemctl daemon-reload
|
||||
systemctl restart campus
|
||||
elif [ -f "$CAMPUS_DROPIN" ]; then
|
||||
rm -f "$CAMPUS_DROPIN"
|
||||
systemctl daemon-reload
|
||||
systemctl restart campus
|
||||
echo " [tachyon] not running -> voice reverted to Photon (removed campus drop-in)"
|
||||
fi
|
||||
fi
|
||||
'@
|
||||
# Force LF line endings + UTF-8 no BOM (bash rejects CRLF shebang / BOM).
|
||||
$installLF = $install -replace "`r`n","`n"
|
||||
[IO.File]::WriteAllBytes("$out\install.sh", [Text.UTF8Encoding]::new($false).GetBytes($installLF))
|
||||
|
||||
Write-Host " [4/4] Packing $pkg ..."
|
||||
if (Test-Path $pkg) { Remove-Item $pkg -Force }
|
||||
# tar is native on Windows 10+; -C to switch into the publish dir so paths in the
|
||||
# tarball are relative ("./install.sh" etc.) - what the installer expects.
|
||||
tar -czf $pkg -C $out .
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host " [X] tar failed"; pause; exit 1 }
|
||||
|
||||
$szMB = [math]::Round((Get-Item $pkg).Length / 1MB, 1)
|
||||
Write-Host ""
|
||||
Write-Host " ==========================================="
|
||||
Write-Host " PACKAGED OK"
|
||||
Write-Host (" file : $pkg ({0} MB)" -f $szMB)
|
||||
Write-Host " build : $buildId"
|
||||
Write-Host ""
|
||||
Write-Host " Next: DeployCampusServer.bat"
|
||||
Write-Host " ==========================================="
|
||||
Reference in New Issue
Block a user