├── server ├── .nvmrc ├── package.json ├── Dockerfile ├── screeps-cli.cjs └── screeps-start.cjs ├── .github ├── CODEOWNERS ├── workflows │ ├── release.yml │ ├── release-drafter.yml │ ├── publish-wiki.yml │ ├── deploy-image.yml │ └── test.yml ├── renovate.json └── release-drafter.yml ├── .gitignore ├── .env.sample ├── wiki ├── README.md ├── Updating.md ├── Troubleshooting.md ├── Configuration.md └── Getting-started.md ├── test-config.yml ├── config.yml ├── tsconfig.json ├── README.md ├── docker-compose.yml ├── package.json └── LICENSE /server/.nvmrc: -------------------------------------------------------------------------------- 1 | 10.24.1 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Jomik 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | STEAM_KEY="" 2 | -------------------------------------------------------------------------------- /wiki/README.md: -------------------------------------------------------------------------------- 1 | # Jomik's Screeps Server 2 | 3 | Welcome to the wiki! You should be able to find an answer to your questions here. 4 | If you cannot, please open an issue! 5 | 6 | You should start by reading the [Getting started](Getting-started.md) page. 7 | -------------------------------------------------------------------------------- /test-config.yml: -------------------------------------------------------------------------------- 1 | mods: 2 | - screepsmod-auth 3 | - screepsmod-admin-utils 4 | - screepsmod-cli 5 | bots: 6 | simplebot: screepsbot-zeswarm 7 | 8 | cli: 9 | host: 0.0.0.0 10 | port: 21028 11 | 12 | launcherOptions: 13 | runnerThreads: 1 14 | processorCount: 1 15 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | mods: 2 | - screepsmod-auth 3 | - screepsmod-admin-utils 4 | - screepsmod-mongo 5 | bots: 6 | simplebot: screepsbot-zeswarm 7 | 8 | launcherOptions: 9 | # If set, automatically ensures all mods are updated 10 | autoUpdate: false 11 | # If set, forward console messages to terminal 12 | logConsole: false 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node10", 6 | "allowJs": true, 7 | "checkJs": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 5 * * *" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Publish Latest Release 13 | uses: ivangabriele/publish-latest-release@df1a4afd8aea9d1f0ba5ebeb89452aeac7bca0a9 # v3 14 | env: 15 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 16 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screeps-server", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "repository": "https://github.com/Jomik/screeps-server", 7 | "author": "Jonas Holst Damtoft ", 8 | "license": "MIT", 9 | "engines": { 10 | "npm": "~6", 11 | "node": "10 - 12" 12 | }, 13 | "dependencies": { 14 | "screeps": "4.2.19", 15 | "js-yaml": "4.1.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request_target: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | update_release_draft: 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/publish-wiki.yml: -------------------------------------------------------------------------------- 1 | name: Publish wiki 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - wiki/** 7 | - .github/workflows/publish-wiki.yml 8 | 9 | concurrency: 10 | group: publish-wiki 11 | cancel-in-progress: true 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | publish-wiki: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 21 | - uses: Andrew-Chen-Wang/github-wiki-action@6448478bd55f1f3f752c93af8ac03207eccc3213 # master 22 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended", "helpers:pinGitHubActionDigests"], 4 | "lockFileMaintenance": { 5 | "enabled": true, 6 | "automerge": true, 7 | "schedule": ["before 4am"] 8 | }, 9 | "packageRules": [ 10 | { 11 | "description": ["Automatically update to latest minor or patch versions"], 12 | "matchUpdateTypes": ["minor", "patch"], 13 | "matchCurrentVersion": "!/^0/", 14 | "automerge": true 15 | }, 16 | { 17 | "description": ["Ignore unsupported updates"], 18 | "matchPackageNames": ["node", "npm", "@types/node", "mongo"], 19 | "matchManagers": ["dockerfile", "docker-compose", "npm", "nvm"], 20 | "enabled": false 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /wiki/Updating.md: -------------------------------------------------------------------------------- 1 | ### Prerequisites 2 | Check for new prerequisites and breaking changes 3 | 4 | ### Check for new version 5 | You can see the latest images here [Docker Hub](https://hub.docker.com/repository/docker/jomik/screeps-server). 6 | 7 | ### Update docker image 8 | Run `docker compose pull screeps` to download any new version of the image. 9 | You can run `docker compose pull` without the `screeps` specifier to pull all new images for the compose file. 10 | 11 | ### Update mods and bots 12 | Mods and bots can be updated by running `docker compose exec screeps start --update` and restarting the server. This will go over the 13 | installed packages and update any that are not pinned to their latest available version. You can pin a package 14 | to specific version in the config.yml, like so: `mod@1.2.3`. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is an alternative to [screepers/screeps-launcher]. 2 | 3 | ## Why? 4 | 5 | I believe that build and setup should happen during the build of a docker image. 6 | The [screepers/screeps-launcher] does all setup and installation during the run of the image. 7 | 8 | This image does all installation and setup during the build stage. 9 | So to launch the server, it will only start the server. 10 | Mods and bots are managed at startup by checking your `config.yml`. 11 | `npm` is only invoked if changes are made to your `config.yml`. 12 | 13 | ## Getting started, configuration, updating and troubleshooting 14 | 15 | Please check the [wiki](https://github.com/Jomik/screeps-server/wiki)! I will try to keep that one up to date. 16 | 17 | [screepers/screeps-launcher]: https://github.com/screepers/screeps-launcher 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | screeps: 3 | image: ghcr.io/jomik/screeps-server:edge 4 | depends_on: 5 | - mongo 6 | - redis 7 | ports: 8 | - 21025:21025/tcp 9 | volumes: 10 | - ./config.yml:/screeps/config.yml 11 | - screeps-user:/home/node 12 | - screeps-data:/data 13 | - screeps-mods:/screeps/mods 14 | environment: 15 | MONGO_HOST: mongo 16 | REDIS_HOST: redis 17 | STEAM_KEY: ${STEAM_KEY:?"Missing steam key"} 18 | restart: unless-stopped 19 | build: server 20 | 21 | mongo: 22 | image: mongo:4.4.18 23 | volumes: 24 | - mongo-data:/data/db 25 | restart: unless-stopped 26 | 27 | redis: 28 | image: redis:7 29 | volumes: 30 | - redis-data:/data 31 | restart: unless-stopped 32 | 33 | volumes: 34 | screeps-data: 35 | screeps-mods: 36 | screeps-user: 37 | redis-data: 38 | mongo-data: 39 | -------------------------------------------------------------------------------- /wiki/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | ### Help, my server is running but I can't connect. 2 | - Follow the instructions for [screepsmod-auth](https://github.com/ScreepsMods/screepsmod-auth) 3 | ### I can't push any code via `rollup` to my server. 4 | - Make sure your `screeps.json` configuration in your project is set properly. 5 | - In your `email:` field, simply put in your `username`. Verify your password is the same as your `screepsmod-auth` setting. 6 | ### My map is all red, I can't actually spawn in! 7 | 8 | - This is most likely a result of your map not loaded properly on first-run. To fix it do the following. 9 | 10 | - Step 1: Navigate to your server file location in terminal/powershell. 11 | - Step 2: Run `docker compose exec screeps cli` 12 | - Step 3: Run `system.resetAllData()` and reconnect. 13 | 14 | - Restart your server, check your configuration and follow the instructions for [screepsmod-admin-utils](https://github.com/ScreepsMods/screepsmod-admin-utils) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screeps-server", 3 | "private": true, 4 | "version": "1.0.0", 5 | "repository": "https://github.com/Jomik/screeps-server", 6 | "author": "Jonas Holst Damtoft ", 7 | "license": "MIT", 8 | "scripts": { 9 | "check": "tsc -p tsconfig.json", 10 | "start": "docker compose up -d", 11 | "start:logs": "npm run start && npm run logs", 12 | "stop": "docker compose stop", 13 | "restart": "docker compose restart screeps", 14 | "restart:logs": "npm run restart && npm run logs", 15 | "reset": "docker compose down", 16 | "reset:hard": "docker compose down -v", 17 | "logs": "docker compose logs -ft -n 100 screeps", 18 | "shell": "docker compose exec screeps /bin/sh", 19 | "redis-cli": "docker compose exec redis redis-cli", 20 | "cli": "docker compose exec screeps cli", 21 | "update": "docker compose exec screeps start --update" 22 | }, 23 | "devDependencies": { 24 | "@types/js-yaml": "^4.0.9", 25 | "@types/node": "^10.17.60", 26 | "typescript": "^5.6.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jonas Holst Damtoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "Release v$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 4 | change-title-escapes: '\<*_&' 5 | exclude-contributors: 6 | - "renovate" 7 | categories: 8 | - title: "🚀 Features" 9 | labels: 10 | - feature 11 | - enhancement 12 | - title: "🐛 Bug Fixes" 13 | labels: 14 | - bug 15 | - title: "🧰 Maintenance" 16 | labels: 17 | - chore 18 | - title: "Github" 19 | labels: 20 | - "area/github" 21 | autolabeler: 22 | - label: "chore" 23 | files: 24 | - "*.md" 25 | branch: 26 | - '/docs{0,1}\/.+/' 27 | title: 28 | - "/chore/" 29 | - label: "bug" 30 | branch: 31 | - '/fix\/.+/' 32 | title: 33 | - "/fix/i" 34 | - label: "enhancement" 35 | branch: 36 | - '/feature\/.+/' 37 | title: 38 | - "/feat/" 39 | version-resolver: 40 | major: 41 | labels: ["type/break"] 42 | minor: 43 | labels: ["type/major", "type/minor"] 44 | patch: 45 | labels: ["type/patch"] 46 | default: patch 47 | template: | 48 | ## What's Changed 49 | 50 | $CHANGES 51 | 52 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 53 | -------------------------------------------------------------------------------- /wiki/Configuration.md: -------------------------------------------------------------------------------- 1 | Edit `config.yml` to add mods and bots. Some mods also look there for configuration. 2 | The mods and bots are installed using `npm install`, thus you can use anything that it recognizes as [a valid package](https://docs.npmjs.com/cli/v6/commands/npm-install). 3 | 4 | ### Defaults 5 | We currently add the following mods, as default: 6 | 7 | - [screepsmod-auth](https://github.com/ScreepsMods/screepsmod-auth) 8 | - [screepsmod-admin-utils](https://github.com/ScreepsMods/screepsmod-admin-utils) 9 | - [screepsmod-mongo](https://github.com/ScreepsMods/screepsmod-mongo) 10 | 11 | ### Mods 12 | Mods are managed by changing the `mods` YAML list in `config.yml`. 13 | Here is an example where we remove `mongo` and add `cli` 14 | ```diff 15 | mods: 16 | - screepsmod-auth 17 | - screepsmod-admin-utils 18 | - - screepsmod-mongo 19 | + - screepsmod-cli 20 | ``` 21 | 22 | ### Bots 23 | Bots are managed by changing the `bots` object in `config.yml`. The key is the name you wish to use for the bot, the value is the package. 24 | ```diff 25 | bots: 26 | - simplebot: screepsbot-zeswarm 27 | + zeswarm: screepsbot-zeswarm 28 | + quorom: screepsbot-quorum 29 | ``` 30 | 31 | ### Server options 32 | We can set options specific to the server. Here is an example that forwards log messages to the terminal. 33 | ```diff 34 | serverOptions: 35 | - logConsole: false 36 | + logConsole: true 37 | ``` -------------------------------------------------------------------------------- /wiki/Getting-started.md: -------------------------------------------------------------------------------- 1 | ### Prerequisites 2 | 3 | A working [Docker](https://www.docker.com/) installation. 4 | You can use [Docker Desktop](https://www.docker.com/products/docker-desktop/) 5 | 6 | ### Setup 7 | 8 | Download the [compose file](./docker-compose.yml), [envfile](./.env.sample) and [configuration](./config.yml) file to your computer. You can put this in your Screeps project. 9 | Copy `.env.sample` to `.env`, this can hold secrets for you, and should be ignored in git! 10 | 11 | You can use this command (in a shell) to do the above, in your current directory. 12 | 13 | ```sh 14 | curl --remote-name-all https://raw.githubusercontent.com/Jomik/screeps-server/main/{docker-compose.yml,.env.sample,config.yml} && cp .env.sample .env && echo ".env" >> .gitignore 15 | ``` 16 | 17 | Paste the the value labeled `Key` from your [Steam API key](https://steamcommunity.com/dev/apikey) into `.env`. 18 | ### Starting the server 19 | 20 | In your project run `docker compose up -d`. 21 | Run `docker compose logs screeps -f` to view and follow the logs for the screeps-server container. 22 | To stop following the logs, press `CTRL + C`. 23 | Assuming nothing went wrong, you should be able to connect to your server on `http://localhost:21025`. 24 | 25 | ### Stopping the server 26 | In your project run `docker compose stop`. This will stop the containers, but not remove them, so starting is quicker again. 27 | 28 | To __fully__ wipe the server and its data, run `docker compose down -v`. This removes containers, networks and volumes. 29 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=10 2 | FROM node:${NODE_VERSION}-alpine AS screeps 3 | 4 | # Install node-gyp dependencies 5 | # We do not pin as we use multiple node versions. 6 | # They are so old that there is no changes to their package registry anyway.. 7 | # hadolint ignore=DL3018 8 | RUN --mount=type=cache,target=/etc/apk/cache \ 9 | apk add --no-cache bash python2 make gcc g++ 10 | 11 | # Install screeps 12 | WORKDIR /screeps 13 | COPY package.json package-lock.json ./ 14 | RUN --mount=type=cache,target=/root/.npm \ 15 | npm clean-install 16 | 17 | # Initialize screeps, similar to `screeps init` 18 | RUN cp -a /screeps/node_modules/@screeps/launcher/init_dist/.screepsrc ./ && \ 19 | cp -a /screeps/node_modules/@screeps/launcher/init_dist/db.json ./ && \ 20 | cp -a /screeps/node_modules/@screeps/launcher/init_dist/assets/ ./ 21 | 22 | # Gotta remove this Windows carriage return shenanigans 23 | RUN sed -i "s/\r//" .screepsrc 24 | 25 | FROM node:${NODE_VERSION}-alpine AS server 26 | # hadolint ignore=DL3018 27 | RUN --mount=type=cache,target=/var/cache/apk \ 28 | apk add --no-cache git 29 | 30 | USER node 31 | COPY --from=screeps --chown=node:node /screeps /screeps/ 32 | 33 | # Move the database file to shared directory 34 | WORKDIR /data 35 | RUN mv /screeps/db.json /data/db.json && \ 36 | sed -i "s/db = db.json/db = \/data\/db.json/" /screeps/.screepsrc 37 | RUN mv /screeps/assets /data/assets && \ 38 | sed -i "s/assetdir = assets/assetdir = \/data\/assets/" /screeps/.screepsrc 39 | 40 | WORKDIR /screeps 41 | 42 | # Init mods package 43 | RUN mkdir ./mods && echo "{}" > ./mods/package.json 44 | 45 | COPY screeps-cli.cjs ./bin/cli 46 | COPY screeps-start.cjs ./bin/start 47 | 48 | ENV SERVER_DIR=/screeps NODE_ENV=production PATH="/screeps/bin:${PATH}" 49 | 50 | VOLUME [ "/data" ] 51 | EXPOSE 21025 52 | 53 | HEALTHCHECK --start-period=10s --interval=30s --timeout=3s \ 54 | CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:21025/api/version || exit 1 55 | 56 | ENTRYPOINT ["start"] 57 | -------------------------------------------------------------------------------- /.github/workflows/deploy-image.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Images to GHCR 2 | 3 | env: 4 | REGISTRY: ghcr.io 5 | IMAGE_NAME: ${{ github.repository }} 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | tags: 12 | - "v*" 13 | release: 14 | types: 15 | - published 16 | 17 | workflow_dispatch: 18 | 19 | jobs: 20 | push-image: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | node: [10, 12] 25 | permissions: 26 | contents: read 27 | packages: write 28 | steps: 29 | - name: "Checkout GitHub Action" 30 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 31 | - name: Log in to the Container registry 32 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 33 | with: 34 | registry: ${{ env.REGISTRY }} 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 39 | - name: Extract metadata (tags, labels) for Docker 40 | id: meta 41 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 42 | with: 43 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 44 | github-token: ${{ github.token }} 45 | flavor: | 46 | latest=auto 47 | suffix=${{ matrix.node != 10 && format('-node{0}', matrix.node) || '' }},onlatest=true 48 | tags: | 49 | type=edge,branch=main 50 | type=semver,pattern={{version}} 51 | type=semver,pattern={{major}}.{{minor}} 52 | type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} 53 | type=sha 54 | - name: Build and push Docker image 55 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 56 | with: 57 | context: server 58 | push: true 59 | build-args: | 60 | NODE_VERSION=${{ matrix.node }} 61 | tags: ${{ steps.meta.outputs.tags }} 62 | labels: ${{ steps.meta.outputs.labels }} 63 | platforms: linux/amd64,linux/arm64 64 | cache-from: type=gha 65 | cache-to: type=gha,mode=max 66 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test server image 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - "main" 7 | 8 | env: 9 | TEST_TAG: jomik/screeps-server:test 10 | 11 | permissions: "read-all" 12 | 13 | jobs: 14 | test-image: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | node: [10, 12] 20 | steps: 21 | - name: Get merge commit sha 22 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 23 | id: pr 24 | with: 25 | result-encoding: string 26 | script: | 27 | const { data } = await github.rest.pulls.get({ 28 | ...context.repo, 29 | pull_number: context.payload.pull_request.number, 30 | }); 31 | return data.merge_commit_sha; 32 | - name: Checkout merge commit 33 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 34 | with: 35 | ref: ${{ steps.pr.outputs.result }} 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 38 | - name: Build 39 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 40 | with: 41 | context: server 42 | build-args: | 43 | NODE_VERSION=${{ matrix.node }} 44 | cache-from: type=gha 45 | cache-to: type=gha,mode=max 46 | tags: ${{ env.TEST_TAG }} 47 | load: true 48 | - name: Start the container 49 | run: | 50 | docker run -d -p 21025:21025 -p 21028:21028 --env STEAM_KEY --name screeps -v ${CONFIG_FILE}:/screeps/config.yml ${TEST_TAG} 51 | env: 52 | STEAM_KEY: ${{ secrets.STEAM_KEY }} 53 | CONFIG_FILE: ${{ format('{0}/{1}', github.workspace, 'test-config.yml') }} 54 | - name: Wait for container to be healthy 55 | uses: stringbean/docker-healthcheck-action@a958d329225ccbd485766815734e01c335e62bd4 # v3 56 | with: 57 | container: screeps 58 | wait-time: 60 59 | require-status: running 60 | require-healthy: true 61 | - name: Show container logs 62 | if: always() 63 | run: docker container logs screeps 64 | - name: Check that mods are registered 65 | run: | 66 | set -eu 67 | server_data=$(curl http://localhost:21025/api/version | jq -c '.serverData') 68 | echo $server_data | jq -e '.features | any(.name == "screepsmod-auth")' 69 | echo $server_data | jq -e '.features | any(.name == "screepsmod-admin-utils")' 70 | echo $server_data | jq -e '.features | any(.name == "screepsmod-cli")' 71 | - name: Check that bots are registered 72 | run: | 73 | set -eu 74 | bots=$(curl -X POST http://localhost:21028/cli -d "help(bots)" | grep -A 10 "Bot AIs:") 75 | echo $bots | grep 'simplebot' | grep "screepsbot-zeswarm" 76 | - name: Stop container 77 | if: always() 78 | run: docker container stop screeps 79 | -------------------------------------------------------------------------------- /server/screeps-cli.cjs: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | // @ts-ignore We can't load that from the outer non-Node 10 side 3 | const repl = require('repl'); 4 | const q = require('q'); 5 | const net = require('net'); 6 | const path = require('path'); 7 | const os = require('os'); 8 | const fs = require('fs'); 9 | const vm = require('vm'); 10 | const readline = require('readline'); 11 | 12 | const HISTORY_FILE = (() => { 13 | const filePath = process.env.CLI_HISTORY_FILE; 14 | if (filePath) { 15 | if (path.isAbsolute(filePath)) { 16 | return filePath; 17 | } else { 18 | return path.normalize(path.join(os.homedir(), filePath)); 19 | } 20 | } 21 | return path.join(os.homedir(), '.screeps-history'); 22 | })(); 23 | 24 | /** 25 | * @param {string} host 26 | * @param {number} port 27 | * @param {string} [cmd] 28 | * @returns 29 | */ 30 | function cli(host, port, cmd = undefined) { 31 | 32 | const defer = q.defer(); 33 | 34 | const socket = net.connect(port, host); 35 | /** @type {repl.REPLServer} */ 36 | let rl; 37 | let connected = false; 38 | 39 | /** 40 | * Send a command to the server for execution 41 | * @param {string} input 42 | */ 43 | const executeCommand = (input) => { 44 | // The server side feeds the socket through `readline`, which splits on 45 | // newlines. To avoid breaking multi-line input into multiple commands, 46 | // we collapse internal newlines into spaces before sending. 47 | const toSend = input 48 | .replace(/\r?\n$/, '') // drop the final newline REPL adds 49 | .replace(/\r?\n/g, ' '); // turn internal newlines into spaces 50 | 51 | socket.write(toSend + "\r\n"); 52 | } 53 | 54 | /** 55 | * Evaluate the REPL input 56 | * @param {string} input 57 | * @param {vm.Context} context 58 | * @param {string} filename 59 | * @param {(err: Error | null, result?: any) => void} callback 60 | */ 61 | const replEval = (input, context, filename, callback) => { 62 | try { 63 | // Using "vm.Script" lets use the V8 parser to check for syntax validity. 64 | new vm.Script(input, { filename }); 65 | } catch (err) { 66 | if (!(err instanceof Error)) { 67 | console.error('Unexpected error from repl eval', err); 68 | process.exit(1); 69 | return; 70 | } 71 | if (isRecoverableError(err)) { 72 | return callback(new repl.Recoverable(err)); 73 | } 74 | return callback(err); 75 | } 76 | 77 | // At this point the input is complete JS. Pass the whole buffered input 78 | // to the socket, so multi-line constructs (like function definitions) 79 | // are already combined. 80 | executeCommand(input); 81 | callback(null); 82 | }; 83 | 84 | /** 85 | * Decide whether a syntax error is recoverable (i.e. REPL should keep 86 | * accepting more input instead of erroring immediately). 87 | * 88 | * @param {Error} error 89 | * @returns {boolean} 90 | */ 91 | function isRecoverableError(error) { 92 | if (error.name === 'SyntaxError') { 93 | return /^(Unexpected end of input|Unexpected token)/.test(error.message); 94 | } 95 | return false; 96 | } 97 | 98 | socket.on('connect', () => { 99 | connected = true; 100 | 101 | if (cmd) { 102 | // Running in command mode, we're just gonna send the provided command, 103 | // wait for an answer and exit immediately. 104 | socket.on("data", data => { 105 | const string = data.toString('utf8'); 106 | const cleaned = string.replace(/^< /, '').replace(/\n< /g, '\n'); 107 | if (cleaned.match(/^Screeps server v.* running on port .*/)) { 108 | // Skip over server connection answer 109 | return; 110 | } 111 | 112 | process.stdout.write(cleaned); 113 | process.exit(1); 114 | }); 115 | executeCommand(cmd); 116 | return; 117 | } 118 | 119 | defer.resolve(); 120 | rl = repl.start({ 121 | input: process.stdin, 122 | output: process.stdout, 123 | prompt: "> ", 124 | eval: replEval, 125 | }); 126 | 127 | try { 128 | // @ts-expect-error I'm guessing this is a private ivar of REPL? 129 | rl.history = JSON.parse(fs.readFileSync(HISTORY_FILE).toString('utf8')); 130 | } catch (err) {} 131 | 132 | rl.on('close', () => { 133 | // @ts-expect-error I'm guessing this is a private ivar of REPL? 134 | fs.writeFileSync(HISTORY_FILE, JSON.stringify(rl.history)); 135 | socket.end(); 136 | }); 137 | 138 | rl.on('exit', () => { 139 | rl.output.write(`Disconnecting…\r\n`); 140 | socket.end(); 141 | }); 142 | 143 | rl.output.write(`Screeps CLI connected on ${host}:${port}.\r\n-----------------------------------------\r\n`); 144 | }); 145 | 146 | socket.on('data', (data) => { 147 | if (!rl) return; 148 | const string = data.toString('utf8'); 149 | const cleaned = string.replace(/^< /, '').replace(/\n< /g, '\n'); 150 | 151 | // Clear the current input line (prompt + user-typed text), 152 | // print the server output, then redraw the prompt and buffer so 153 | // asynchronous logs don't interleave with what the user is typing. 154 | readline.clearLine(rl.output, 0); 155 | readline.cursorTo(rl.output, 0); 156 | rl.output.write(cleaned); 157 | if (!/\n$/.test(cleaned)) { 158 | rl.output.write('\n'); 159 | } 160 | rl.displayPrompt(true); 161 | }); 162 | 163 | socket.on('error', (error) => { 164 | if (!connected) { 165 | console.error(`Failed to connect to ${host}:${port}: ${error.message}`); 166 | } else { 167 | console.error(`Socket error: ${error.message}`); 168 | } 169 | defer.reject(error); 170 | process.exit(1); 171 | }); 172 | 173 | socket.on('close', () => { 174 | if (rl) { 175 | rl.close(); 176 | } 177 | process.exit(0); 178 | }); 179 | 180 | return defer.promise; 181 | }; 182 | 183 | // Command line options and arguments 184 | /** @type {string | undefined} */ 185 | let host = undefined; 186 | /** @type {number | undefined} */ 187 | let port = undefined; 188 | /** @type {string | undefined} */ 189 | let command = undefined; 190 | 191 | // Janky option parsing 192 | const argStart = process.argv.findIndex(arg => arg === __filename) + 1; 193 | const ARGV = process.argv.slice(argStart); 194 | while (ARGV.length) { 195 | if (ARGV[0][0] === "-") { 196 | if (ARGV[0] === "-c") { 197 | ARGV.shift() 198 | command = ARGV.shift(); 199 | } else { 200 | console.error(`Unknown option ${ARGV[0]}`); 201 | } 202 | } else { 203 | if (host === undefined) { 204 | host = ARGV.shift(); 205 | } else if (port === undefined) { 206 | const portStr = ARGV.shift(); 207 | if (portStr === undefined) { 208 | console.error(`Missing port number ${portStr}`); 209 | process.exit(1); 210 | } 211 | const portNum = parseInt(portStr, 10); 212 | if (isNaN(portNum)) { 213 | console.error(`Invalid port number ${portStr}`); 214 | process.exit(1); 215 | } 216 | port = portNum; 217 | } else { 218 | console.error(`Unknown argument ${ARGV[0]}`); 219 | process.exit(1); 220 | } 221 | } 222 | } 223 | 224 | host = host || "localhost"; 225 | port = port || 21026; 226 | 227 | cli(host, port, command); 228 | -------------------------------------------------------------------------------- /server/screeps-start.cjs: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const yaml = require("js-yaml"); 5 | const { execSync } = require("child_process"); 6 | 7 | const RootDir = process.env["SERVER_DIR"]; 8 | if (!RootDir) { 9 | throw new Error("Missing environment variable $SERVER_DIR"); 10 | } 11 | const ModsDir = path.join(RootDir, "mods"); 12 | const ConfigPath = path.join(RootDir, "config.yml"); 13 | 14 | process.chdir(RootDir); 15 | 16 | /** 17 | * @typedef LauncherOptions 18 | * @property {boolean} autoUpdate 19 | * @property {boolean} logConsole 20 | * @property {number} runnerThreads 21 | * @property {number} processorCount 22 | * @property {number} storageTimeout 23 | * @property {number} logRotateKeep 24 | * @property {number} restartInterval 25 | */ 26 | 27 | /** 28 | * @typedef Config 29 | * @property {string} steamKey 30 | * @property {string[]} mods 31 | * @property {Record} bots 32 | * @property {LauncherOptions} launcherOptions 33 | */ 34 | 35 | const config = /** @type {Config} */ (yaml.load(fs.readFileSync(ConfigPath, "utf8"))); 36 | 37 | /** 38 | * @param {string} dir 39 | * @returns {any} 40 | */ 41 | const loadPackage = (dir) => 42 | JSON.parse(fs.readFileSync(path.resolve(dir, "package.json"), "utf8")); 43 | 44 | /** 45 | * 46 | * @param {string} pkg 47 | * @param {[string, string]} param 48 | * @returns {boolean} 49 | */ 50 | const isDependency = (pkg, [name, version]) => 51 | pkg.includes(name) || version.includes(pkg); 52 | 53 | const VERSION = /^(=|^|~|<|>|<=|>=)?\d+(?:\.\d+(?:\.\d+(?:.*)?)?)?$/ 54 | 55 | /** 56 | * 57 | * @param {string} spec 58 | * @returns 59 | */ 60 | const parseVersionSpec = (spec) => { 61 | const atIdx = spec.lastIndexOf("@"); 62 | if (atIdx === -1) { 63 | return [spec, "latest"]; 64 | } 65 | const name = spec.substring(0, atIdx); 66 | const version = spec.substring(atIdx + 1); 67 | if (!version.match(VERSION)) { 68 | return [spec, "latest"]; 69 | } 70 | return [name, version]; 71 | } 72 | 73 | const installPackages = () => { 74 | console.log("Updating dependencies"); 75 | const mods = config.mods || []; 76 | const bots = config.bots || {}; 77 | 78 | const modsPackage = loadPackage(ModsDir); 79 | const dependencies = modsPackage.dependencies || {}; 80 | 81 | // Calculate package diff 82 | const packages = [...mods, ...Object.values(bots)]; 83 | 84 | const newPackages = packages.filter( 85 | (pkg) => 86 | !Object.entries(dependencies).some((dependency) => 87 | isDependency(pkg, dependency), 88 | ), 89 | ); 90 | const removedPackages = Object.entries(dependencies).filter( 91 | (dependency) => !packages.some((pkg) => isDependency(pkg, dependency)), 92 | ); 93 | 94 | if (removedPackages.length === 0 && newPackages.length === 0) { 95 | console.log("No dependency changes"); 96 | } 97 | 98 | if (removedPackages.length > 0) { 99 | const packageNames = removedPackages 100 | .map((pkg) => { 101 | const entry = 102 | Object.entries(dependencies).find( 103 | ([name, version]) => pkg.includes(name) || version.includes(pkg), 104 | ) || []; 105 | return entry[0]; 106 | }) 107 | .filter((name) => name !== undefined); 108 | 109 | console.log("Uninstalling", ...packageNames); 110 | execSync( 111 | `npm uninstall --logevel=error --no-progress ${packageNames.join(" ")}`, 112 | { 113 | cwd: ModsDir, 114 | stdio: "inherit", 115 | encoding: "utf8", 116 | }, 117 | ); 118 | } 119 | 120 | if (newPackages.length > 0) { 121 | console.log("Installing", ...newPackages); 122 | execSync( 123 | `npm install --logevel=error --no-progress -E ${newPackages.join(" ")}`, 124 | { 125 | cwd: ModsDir, 126 | stdio: "inherit", 127 | encoding: "utf8", 128 | }, 129 | ); 130 | } 131 | 132 | console.log("Done updating"); 133 | } 134 | 135 | /** 136 | * 137 | * @param {boolean} doUpdate 138 | * @returns 139 | */ 140 | const updatePackages = (doUpdate) => { 141 | const mods = config.mods || []; 142 | const bots = config.bots || {}; 143 | 144 | const modsPackage = loadPackage(ModsDir); 145 | const dependencies = modsPackage.dependencies || {}; 146 | 147 | // Calculate package diff 148 | const configuredPackages = [...mods, ...Object.values(bots)]; 149 | 150 | const packagedMods = configuredPackages.filter( 151 | (pkg) => 152 | Object.entries(dependencies).some((dependency) => 153 | isDependency(pkg, dependency), 154 | ), 155 | ).map((pkg) => parseVersionSpec(pkg)); 156 | 157 | let outdated = {}; 158 | const outdatedFile = path.resolve(ModsDir, "outdated.json"); 159 | try { 160 | // `npm outdated --json` returns 1 if there are outdated packages, 161 | // which causes `execSync` to throw an error. 162 | execSync("npm outdated --json > outdated.json || true", { 163 | cwd: ModsDir, 164 | stdio: "inherit", 165 | encoding: "utf8", 166 | }) 167 | const output = fs.readFileSync(outdatedFile).toString() 168 | outdated = JSON.parse(output); 169 | } catch { 170 | } finally { 171 | try { 172 | fs.unlinkSync(outdatedFile); 173 | } catch { 174 | } 175 | } 176 | 177 | const versionSpecs = []; 178 | for (const [mod, info] of Object.entries(outdated)) { 179 | const [name, version] = packagedMods.find(([pkg]) => mod === pkg) || []; 180 | if (!name) continue; 181 | if (version !== "latest") { 182 | console.log(`package ${name} is pinned to version ${version}, ignoring`); 183 | continue; 184 | } 185 | versionSpecs.push(`${mod}@${info.latest}`); 186 | } 187 | 188 | if (versionSpecs.length === 0) { 189 | console.log(`All mods are up to date!`); 190 | return false; 191 | } 192 | 193 | if (!doUpdate) { 194 | console.log(`There are outdated mods needing an update:`, ...versionSpecs); 195 | return true; 196 | } 197 | 198 | console.log(`Updating outdated mods`, ...versionSpecs); 199 | execSync(`npm install --loglevel=error --no-progress -E ${versionSpecs.join(" ")}`, { 200 | cwd: ModsDir, 201 | stdio: "inherit", 202 | encoding: "utf8", 203 | }); 204 | return false; 205 | }; 206 | 207 | const writeModsConfiguration = () => { 208 | console.log("Writing mods configuration"); 209 | const mods = config.mods || []; 210 | const bots = config.bots || {}; 211 | const { dependencies } = loadPackage(ModsDir); 212 | /** @type {Pick} */ 213 | const modsJSON = { mods: [], bots: {} }; 214 | 215 | for (const [name, version] of Object.entries(dependencies)) { 216 | const pkgDir = path.resolve(ModsDir, "node_modules", name); 217 | const { main } = loadPackage(pkgDir); 218 | if (!main) { 219 | console.warn( 220 | `Missing 'main' key for ${name}, report this to the author of the package.`, 221 | ); 222 | } 223 | const mainPath = path.resolve(pkgDir, main); 224 | 225 | if (mods.some((m) => m.includes(name) || version.includes(m))) { 226 | modsJSON.mods.push(mainPath); 227 | continue; 228 | } 229 | 230 | const bot = Object.entries(bots).find( 231 | ([, dep]) => dep.includes(name) || version.includes(dep), 232 | ); 233 | if (bot) { 234 | modsJSON.bots[bot[0]] = path.dirname(mainPath); 235 | continue; 236 | } 237 | } 238 | 239 | fs.writeFileSync("mods.json", JSON.stringify(modsJSON, null, 2)); 240 | console.log("Mods have been configured"); 241 | }; 242 | 243 | // Map from camelCase to snake_case 244 | const LauncherConfigMap = { 245 | // NOTE: We assume this is outdated and we want one multi thread runner. 246 | // runnerCount: "runners_cnt", 247 | runnerThreads: "runner_threads", 248 | processorCount: "processors_cnt", 249 | storageTimeout: "storage_timeout", 250 | logConsole: "log_console", 251 | logRotateKeep: "log_rotate_keep", 252 | restartInterval: "restart_interval", 253 | }; 254 | 255 | const getPhysicalCores = () => { 256 | const nproc = execSync("nproc --all", { encoding: "utf8" }); 257 | 258 | const cores = Number.parseInt(nproc.trim(), 10); 259 | if (Number.isNaN(cores) && cores < 1) { 260 | console.warn("Error getting number of physical cores, defaulting to 1"); 261 | return 1; 262 | } 263 | return cores; 264 | }; 265 | 266 | const start = async () => { 267 | installPackages(); 268 | writeModsConfiguration(); 269 | 270 | const updateOpt = process.argv.includes("--update"); 271 | const updateNeeded = updatePackages(updateOpt || config.launcherOptions.autoUpdate); 272 | 273 | if (updateOpt) { 274 | process.exit(updateNeeded ? 1 : 0); 275 | } 276 | 277 | // @ts-ignore We can't load that from the outer non-Node 10 side 278 | const screeps = require("@screeps/launcher"); 279 | const cores = getPhysicalCores(); 280 | 281 | /** @type {Record} */ 282 | const options = { 283 | steam_api_key: process.env.STEAM_KEY || config.steamKey, 284 | storage_disable: false, 285 | processors_cnt: cores, 286 | runners_cnt: 1, 287 | runner_threads: Math.max(cores - 1, 1), 288 | }; 289 | 290 | const launcherOptions = config.launcherOptions || {}; 291 | 292 | for (const [configKey, optionsKey] of Object.entries(LauncherConfigMap)) { 293 | if (configKey in launcherOptions) { 294 | // @ts-expect-error Accessing launcherOptions without an string index 295 | options[optionsKey] = launcherOptions[configKey]; 296 | } 297 | } 298 | 299 | await screeps.start(options, process.stdout); 300 | }; 301 | 302 | start().catch((err) => { 303 | console.error(err.message); 304 | process.exit(); 305 | }); 306 | --------------------------------------------------------------------------------