diff --git a/.github/workflows/games.yml b/.github/workflows/games.yml index fc4f7de..2c10b23 100644 --- a/.github/workflows/games.yml +++ b/.github/workflows/games.yml @@ -18,6 +18,7 @@ jobs: game: - fivem - source + - rust steps: - uses: actions/checkout@v2 - uses: docker/setup-buildx-action@v1 diff --git a/games/rust/Dockerfile b/games/rust/Dockerfile new file mode 100644 index 0000000..03295e5 --- /dev/null +++ b/games/rust/Dockerfile @@ -0,0 +1,50 @@ +# +# Copyright (c) 2021 Pterodactyl +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +FROM --platform=$TARGETOS/$TARGETARCH debian:bullseye-slim + +LABEL author="Isaac A." maintainer="isaac@isaacs.site" + +LABEL org.opencontainers.image.source="https://github.com/pterodactyl/yolks" +LABEL org.opencontainers.image.licenses=MIT + +ENV DEBIAN_FRONTEND=noninteractive + +RUN dpkg --add-architecture i386 \ + && apt update \ + && apt upgrade -y \ + && apt install -y lib32gcc-s1 lib32stdc++6 unzip curl iproute2 tzdata libgdiplus libsdl2-2.0-0:i386 \ + && curl -sL https://deb.nodesource.com/setup_14.x | bash - \ + && apt install -y nodejs \ + && mkdir /node_modules \ + && npm install --prefix / ws \ + && useradd -d /home/container -m container + +USER container +ENV USER=container HOME=/home/container + +WORKDIR /home/container + +COPY ./entrypoint.sh /entrypoint.sh +COPY ./wrapper.js /wrapper.js + +CMD [ "/bin/bash", "/entrypoint.sh" ] diff --git a/games/rust/entrypoint.sh b/games/rust/entrypoint.sh new file mode 100644 index 0000000..d300782 --- /dev/null +++ b/games/rust/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash +cd /home/container + +# Make internal Docker IP address available to processes. +export INTERNAL_IP=`ip route get 1 | awk '{print $NF;exit}'` + +# Update Rust Server +./steamcmd/steamcmd.sh +login anonymous +force_install_dir /home/container +app_update 258550 +quit + +# Replace Startup Variables +MODIFIED_STARTUP=`eval echo $(echo ${STARTUP} | sed -e 's/{{/${/g' -e 's/}}/}/g')` +echo ":/home/container$ ${MODIFIED_STARTUP}" + +# OxideMod has been replaced with uMod +if [ -f OXIDE_FLAG ] || [ "${OXIDE}" = 1 ] || [ "${UMOD}" = 1 ]; then + echo "Updating uMod..." + curl -sSL "https://umod.org/games/rust/download/develop" > umod.zip + unzip -o -q umod.zip + rm umod.zip + echo "Done updating uMod!" +fi + +# Fix for Rust not starting +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd) + +# Run the Server +node /wrapper.js "${MODIFIED_STARTUP}" diff --git a/games/rust/wrapper.js b/games/rust/wrapper.js new file mode 100644 index 0000000..1d807a9 --- /dev/null +++ b/games/rust/wrapper.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node + +var startupCmd = ""; +const fs = require("fs"); +fs.writeFile("latest.log", "", (err) => { + if (err) console.log("Callback error in appendFile:" + err); +}); + +var args = process.argv.splice(process.execArgv.length + 2); +for (var i = 0; i < args.length; i++) { + if (i === args.length - 1) { + startupCmd += args[i]; + } else { + startupCmd += args[i] + " "; + } +} + +if (startupCmd.length < 1) { + console.log("Error: Please specify a startup command."); + process.exit(); +} + +const seenPercentage = {}; + +function filter(data) { + const str = data.toString(); + if (str.startsWith("Loading Prefab Bundle ")) { // Rust seems to spam the same percentage, so filter out any duplicates. + const percentage = str.substr("Loading Prefab Bundle ".length); + if (seenPercentage[percentage]) return; + + seenPercentage[percentage] = true; + } + + console.log(str); +} + +var exec = require("child_process").exec; +console.log("Starting Rust..."); + +var exited = false; +const gameProcess = exec(startupCmd); +gameProcess.stdout.on('data', filter); +gameProcess.stderr.on('data', filter); +gameProcess.on('exit', function (code, signal) { + exited = true; + + if (code) { + console.log("Main game process exited with code " + code); + // process.exit(code); + } +}); + +function initialListener(data) { + const command = data.toString().trim(); + if (command === 'quit') { + gameProcess.kill('SIGTERM'); + } else { + console.log('Unable to run "' + command + '" due to RCON not being connected yet.'); + } +} +process.stdin.resume(); +process.stdin.setEncoding("utf8"); +process.stdin.on('data', initialListener); + +process.on('exit', function (code) { + if (exited) return; + + console.log("Received request to stop the process, stopping the game..."); + gameProcess.kill('SIGTERM'); +}); + +var waiting = true; +var poll = function () { + function createPacket(command) { + var packet = { + Identifier: -1, + Message: command, + Name: "WebRcon" + }; + return JSON.stringify(packet); + } + + var serverHostname = process.env.RCON_IP ? process.env.RCON_IP : "localhost"; + var serverPort = process.env.RCON_PORT; + var serverPassword = process.env.RCON_PASS; + var WebSocket = require("ws"); + var ws = new WebSocket("ws://" + serverHostname + ":" + serverPort + "/" + serverPassword); + + ws.on("open", function open() { + console.log("Connected to RCON. Generating the map now. Please wait until the server status switches to \"Running\"."); + waiting = false; + + // Hack to fix broken console output + ws.send(createPacket('status')); + + process.stdin.removeListener('data', initialListener); + gameProcess.stdout.removeListener('data', filter); + gameProcess.stderr.removeListener('data', filter); + process.stdin.on('data', function (text) { + ws.send(createPacket(text)); + }); + }); + + ws.on("message", function (data, flags) { + try { + var json = JSON.parse(data); + if (json !== undefined) { + if (json.Message !== undefined && json.Message.length > 0) { + console.log(json.Message); + const fs = require("fs"); + fs.appendFile("latest.log", "\n" + json.Message, (err) => { + if (err) console.log("Callback error in appendFile:" + err); + }); + } + } else { + console.log("Error: Invalid JSON received"); + } + } catch (e) { + if (e) { + console.log(e); + } + } + }); + + ws.on("error", function (err) { + waiting = true; + console.log("Waiting for RCON to come up..."); + setTimeout(poll, 5000); + }); + + ws.on("close", function () { + if (!waiting) { + console.log("Connection to server closed."); + + exited = true; + process.exit(); + } + }); +} +poll();