From 07157de02a89cefba204345116d23aa82f70d72a Mon Sep 17 00:00:00 2001 From: connlost <4348524953.h@gmail.com> Date: Sun, 14 Jun 2026 01:45:48 +0800 Subject: [PATCH 1/2] Add server config overrides and GHCR builds --- .github/workflows/ghcr-image.yml | 49 +++++++++ Dockerfile | 1 + README.md | 40 +++++++ scripts/compile-parameters.sh | 76 +++++++++++++ tests/compile-parameters-server-config.sh | 124 ++++++++++++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 .github/workflows/ghcr-image.yml create mode 100644 tests/compile-parameters-server-config.sh diff --git a/.github/workflows/ghcr-image.yml b/.github/workflows/ghcr-image.yml new file mode 100644 index 0000000..232ced0 --- /dev/null +++ b/.github/workflows/ghcr-image.yml @@ -0,0 +1,49 @@ +name: GHCR + +on: + push: + branches: + - "**" + +permissions: + contents: read + packages: write + +jobs: + build: + name: Build and push GHCR image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker - GHCR Login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker - Metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=sha,prefix=sha- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Docker - Build / Push + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index dcd6aa2..154df4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -103,6 +103,7 @@ ENV PUID=1000 \ GAME_ID="" \ DATA_PATH="${STEAMAPPDATADIR}" \ MAX_PLAYERS=10 \ + OVERRIDE_SERVER_CONFIG=false \ SEASON="" \ SERVER_IP="" \ SERVER_PORT="" \ diff --git a/README.md b/README.md index d5a3be6..8fe8559 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ These are the arguments you can use to customize server behavior with default va | MODIO_API_KEY | "" | mod.io API key | | MODIO_API_URL | "" | mod.io API path | | MODS | "" | List of mods to install | + ## Mod Support The container supports automatically installing mods from [mod.io](https://mod.io/g/corekeeper). @@ -185,6 +186,45 @@ Server operators must open and forward the necessary ports on their router or fi > Setting a value switches to Direct Connect, which requires opening and forwarding ports.
> Only set this if you specifically want Direct Connect. +## Advanced ServerConfig Overrides + +By default, the image keeps the existing behavior and starts Core Keeper with command-line arguments built from environment variables. Most servers do not need to use this section. + +Set `OVERRIDE_SERVER_CONFIG=true` to additionally generate `/home/steam/core-keeper-data/ServerConfig.generated.json` on each container start and pass it to the server with `-serverconfig`. Use this only for server settings that Core Keeper does not expose as command-line arguments. + +| Argument | Default | Description | +| :---: | :---: | :--- | +| OVERRIDE_SERVER_CONFIG | false | Enables generated `ServerConfig.generated.json` support when set to `true`. | +| MAX_NUMBER_PACKETS_SENT_PER_FRAME | No Default | Only used when `OVERRIDE_SERVER_CONFIG=true`. Sets `maxNumberPacketsSentPerFrame` in the generated `ServerConfig`. If unset, the generated config uses the game default of `1`. | +| NETWORK_SEND_RATE | No Default | Only used when `OVERRIDE_SERVER_CONFIG=true`. Sets `networkSendRate` in the generated `ServerConfig`. If unset, the generated config uses the game default of `20`. | + +The generated file is rebuilt from environment variables every startup, so edit the environment variables instead of editing `ServerConfig.generated.json` by hand. Existing environment variables such as `WORLD_NAME`, `MAX_PLAYERS`, `PASSWORD`, and `SERVER_PORT` are still emitted as command-line arguments, so they continue to work with `OVERRIDE_SERVER_CONFIG=true`. + +### Network send tuning + +`MAX_NUMBER_PACKETS_SENT_PER_FRAME` controls how much snapshot data the server is allowed to send per frame. Raising it can help clients receive dense world areas faster, for example when many automation machines or tiles appear late after players move quickly into a base. Higher values can increase outbound bandwidth usage. + +> [!IMPORTANT] +> In the dedicated server code, Core Keeper calculates the snapshot target size as `min(maxNumberPacketsSentPerFrame * 1200, 9440)` and applies it to Unity NetCode's `NetworkStreamSnapshotTargetSize`. This means values above `8` are unlikely to increase this specific snapshot budget further. + +Example values: + +| MAX_NUMBER_PACKETS_SENT_PER_FRAME | Approximate snapshot budget | Expected effect | +| :---: | :---: | :--- | +| 1 | 1200 bytes/frame | Game default. Lowest bandwidth, but dense areas may stream in slowly. | +| 4 | 4800 bytes/frame | More world/object data can be sent each frame. Good first test for automation-heavy servers. | +| 8 | 9440 bytes/frame | Near the observed server cap. Higher values are unlikely to increase this budget further. | + +`NETWORK_SEND_RATE` controls the server network tick rate. The game default is `20`, and values above the server simulation tick rate may be clamped by the game, so this is usually less useful than increasing `MAX_NUMBER_PACKETS_SENT_PER_FRAME`. Keep it at `20` unless you are specifically testing network tick behavior. + +For automation-heavy servers where tiles or machines appear late but player movement does not rubber-band, start with: + +```env +OVERRIDE_SERVER_CONFIG=true +MAX_NUMBER_PACKETS_SENT_PER_FRAME=4 +NETWORK_SEND_RATE=20 +``` + ### Contributors diff --git a/scripts/compile-parameters.sh b/scripts/compile-parameters.sh index f24660c..4896027 100644 --- a/scripts/compile-parameters.sh +++ b/scripts/compile-parameters.sh @@ -25,6 +25,77 @@ add_flag() { fi } +json_string() { + local value="$1" + + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + value="${value//$'\n'/\\n}" + value="${value//$'\r'/\\r}" + value="${value//$'\t'/\\t}" + + printf '"%s"' "$value" +} + +json_int_or_default() { + local name="$1" + local value="$2" + local default="$3" + + if [ -z "$value" ]; then + printf '%s' "$default" + return + fi + + if [[ "$value" =~ ^-?[0-9]+$ ]]; then + printf '%s' "$value" + return + fi + + echo "Invalid integer value for ${name}: ${value}" >&2 + exit 1 +} + +generate_server_config() { + local data_path="${DATA_PATH:-${STEAMAPPDATADIR:-}}" + local server_config="${data_path}/ServerConfig.generated.json" + local world + local hashed_world_seed + local max_players + local max_number_packets_sent_per_frame + local network_send_rate + local world_mode + local season_override + + world="$(json_int_or_default "WORLD_INDEX" "${WORLD_INDEX:-}" 0)" || exit 1 + hashed_world_seed="$(json_int_or_default "HASHED_WORLD_SEED" "${HASHED_WORLD_SEED:-}" 0)" || exit 1 + max_players="$(json_int_or_default "MAX_PLAYERS" "${MAX_PLAYERS:-}" 10)" || exit 1 + max_number_packets_sent_per_frame="$(json_int_or_default "MAX_NUMBER_PACKETS_SENT_PER_FRAME" "${MAX_NUMBER_PACKETS_SENT_PER_FRAME:-}" 1)" || exit 1 + network_send_rate="$(json_int_or_default "NETWORK_SEND_RATE" "${NETWORK_SEND_RATE:-}" 20)" || exit 1 + world_mode="$(json_int_or_default "WORLD_MODE" "${WORLD_MODE:-}" 0)" || exit 1 + season_override="$(json_int_or_default "SEASON" "${SEASON:-}" -1)" || exit 1 + + mkdir -p "$(dirname "$server_config")" + + cat > "$server_config" <&2 + exit 1 +} + +contains_param_pair() { + local key="$1" + local value="$2" + local index + + for index in "${!params[@]}"; do + if [[ "${params[$index]}" == "$key" && "${params[$((index + 1))]:-}" == "$value" ]]; then + return 0 + fi + done + + return 1 +} + +contains_param() { + local key="$1" + local item + + for item in "${params[@]}"; do + if [[ "$item" == "$key" ]]; then + return 0 + fi + done + + return 1 +} + +reset_env() { + export STEAMAPPDIR="$tmp_root/server" + export STEAMAPPDATADIR="$tmp_root/data" + export WORLD_INDEX=0 + export WORLD_NAME="Core Keeper Server" + export WORLD_SEED="" + export WORLD_MODE=0 + export HASHED_WORLD_SEED="" + export GAME_ID="" + export DATA_PATH="$STEAMAPPDATADIR" + export MAX_PLAYERS=10 + export SEASON="" + export SERVER_IP="" + export SERVER_PORT="" + export ACTIVATE_CONTENT="" + export PASSWORD="" + export ALLOW_ONLY_PLATFORM="" + export ACTIVATE_ALL_CONTENT=false + export OVERRIDE_SERVER_CONFIG="" + export MAX_NUMBER_PACKETS_SENT_PER_FRAME="" + export NETWORK_SEND_RATE="" + + mkdir -p "$STEAMAPPDIR" "$STEAMAPPDATADIR" +} + +compile_parameters() { + params=() + logfile="" + # shellcheck source=../scripts/compile-parameters.sh + source "$repo_root/scripts/compile-parameters.sh" >/dev/null +} + +reset_env +compile_parameters + +if contains_param "-serverconfig"; then + fail "-serverconfig should not be emitted unless OVERRIDE_SERVER_CONFIG=true" +fi + +if [[ -e "$STEAMAPPDATADIR/ServerConfig.generated.json" ]]; then + fail "generated ServerConfig should not be written unless OVERRIDE_SERVER_CONFIG=true" +fi + +reset_env +export OVERRIDE_SERVER_CONFIG=true +export MAX_NUMBER_PACKETS_SENT_PER_FRAME=4 +export NETWORK_SEND_RATE=20 +export WORLD_NAME='Farm "One"' +export WORLD_SEED="seed-value" +export HASHED_WORLD_SEED=123 +export GAME_ID="ABCDEFGHIJKLMNOP" +export MAX_PLAYERS=12 +export SEASON=3 +export PASSWORD="direct-pass" + +compile_parameters + +config_path="$STEAMAPPDATADIR/ServerConfig.generated.json" + +contains_param_pair "-serverconfig" "$config_path" || fail "expected -serverconfig $config_path" +contains_param_pair "-worldname" 'Farm "One"' || fail "existing WORLD_NAME CLI parameter was not preserved" +contains_param_pair "-maxplayers" "12" || fail "existing MAX_PLAYERS CLI parameter was not preserved" +[[ -f "$config_path" ]] || fail "generated ServerConfig file was not created" + +grep -F '"worldName": "Farm \"One\""' "$config_path" >/dev/null || fail "worldName was not JSON escaped correctly" +grep -F '"worldSeed": "seed-value"' "$config_path" >/dev/null || fail "worldSeed missing from generated config" +grep -F '"hashedWorldSeed": 123' "$config_path" >/dev/null || fail "hashedWorldSeed missing from generated config" +grep -F '"maxNumberPlayers": 12' "$config_path" >/dev/null || fail "maxNumberPlayers missing from generated config" +grep -F '"maxNumberPacketsSentPerFrame": 4' "$config_path" >/dev/null || fail "maxNumberPacketsSentPerFrame missing from generated config" +grep -F '"networkSendRate": 20' "$config_path" >/dev/null || fail "networkSendRate missing from generated config" +grep -F '"seasonOverride": 3' "$config_path" >/dev/null || fail "seasonOverride missing from generated config" +grep -F '"password": "direct-pass"' "$config_path" >/dev/null || fail "password missing from generated config" + +reset_env +export OVERRIDE_SERVER_CONFIG=false +export MAX_NUMBER_PACKETS_SENT_PER_FRAME=8 +export NETWORK_SEND_RATE=20 + +compile_parameters + +if contains_param "-serverconfig"; then + fail "-serverconfig should not be emitted when OVERRIDE_SERVER_CONFIG=false" +fi + +echo "compile-parameters-server-config: ok" From ca528a7d1a4cb295cd0989d6a3265abd25b34f35 Mon Sep 17 00:00:00 2001 From: connlost <4348524953.h@gmail.com> Date: Tue, 16 Jun 2026 00:35:14 +0800 Subject: [PATCH 2/2] Add custom launch parameters --- Dockerfile | 1 + README.md | 13 ++++ scripts/compile-parameters.sh | 91 +++++++++++++++++++++++ tests/compile-parameters-server-config.sh | 12 +++ 4 files changed, 117 insertions(+) diff --git a/Dockerfile b/Dockerfile index 154df4e..55a4105 100644 --- a/Dockerfile +++ b/Dockerfile @@ -104,6 +104,7 @@ ENV PUID=1000 \ DATA_PATH="${STEAMAPPDATADIR}" \ MAX_PLAYERS=10 \ OVERRIDE_SERVER_CONFIG=false \ + CUSTOM_LAUNCH_PARAMS="" \ SEASON="" \ SERVER_IP="" \ SERVER_PORT="" \ diff --git a/README.md b/README.md index 8fe8559..038d117 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ Set `OVERRIDE_SERVER_CONFIG=true` to additionally generate `/home/steam/core-kee | OVERRIDE_SERVER_CONFIG | false | Enables generated `ServerConfig.generated.json` support when set to `true`. | | MAX_NUMBER_PACKETS_SENT_PER_FRAME | No Default | Only used when `OVERRIDE_SERVER_CONFIG=true`. Sets `maxNumberPacketsSentPerFrame` in the generated `ServerConfig`. If unset, the generated config uses the game default of `1`. | | NETWORK_SEND_RATE | No Default | Only used when `OVERRIDE_SERVER_CONFIG=true`. Sets `networkSendRate` in the generated `ServerConfig`. If unset, the generated config uses the game default of `20`. | +| CUSTOM_LAUNCH_PARAMS | "" | Appends extra launch arguments after the built-in container arguments. Intended for mod-specific or advanced server flags. | The generated file is rebuilt from environment variables every startup, so edit the environment variables instead of editing `ServerConfig.generated.json` by hand. Existing environment variables such as `WORLD_NAME`, `MAX_PLAYERS`, `PASSWORD`, and `SERVER_PORT` are still emitted as command-line arguments, so they continue to work with `OVERRIDE_SERVER_CONFIG=true`. @@ -225,6 +226,18 @@ MAX_NUMBER_PACKETS_SENT_PER_FRAME=4 NETWORK_SEND_RATE=20 ``` +### Custom launch parameters + +Use `CUSTOM_LAUNCH_PARAMS` for additional dedicated server launch arguments that are not covered by the container's normal environment variables. + +```env +CUSTOM_LAUNCH_PARAMS=-keepserverrunning +CUSTOM_LAUNCH_PARAMS=-keepserverrunning -someflag -someparam value +CUSTOM_LAUNCH_PARAMS=-someparam "value with spaces" +``` + +Standalone flags and `-flag value` pairs can be mixed in the same value. Single-quoted and double-quoted values are preserved as one argument. These arguments are appended after the container's built-in arguments, so use them only for settings that do not already have a dedicated environment variable above. + ### Contributors diff --git a/scripts/compile-parameters.sh b/scripts/compile-parameters.sh index 4896027..a69899a 100644 --- a/scripts/compile-parameters.sh +++ b/scripts/compile-parameters.sh @@ -25,6 +25,95 @@ add_flag() { fi } +append_custom_launch_params() { + local input="${CUSTOM_LAUNCH_PARAMS:-}" + local current="" + local quote="" + local char + local escape=0 + local token_started=0 + local i + local -a custom_params=() + + if [ -z "$input" ]; then + return + fi + + for ((i = 0; i < ${#input}; i++)); do + char="${input:i:1}" + + if [ "$escape" -eq 1 ]; then + current+="$char" + token_started=1 + escape=0 + continue + fi + + if [ "$quote" = "\"" ]; then + if [ "$char" = "\\" ]; then + escape=1 + elif [ "$char" = "\"" ]; then + quote="" + else + current+="$char" + token_started=1 + fi + continue + fi + + if [ "$quote" = "'" ]; then + if [ "$char" = "'" ]; then + quote="" + else + current+="$char" + token_started=1 + fi + continue + fi + + case "$char" in + [[:space:]]) + if [ "$token_started" -eq 1 ]; then + custom_params+=("$current") + current="" + token_started=0 + fi + ;; + "'") + quote="'" + token_started=1 + ;; + "\"") + quote="\"" + token_started=1 + ;; + "\\") + escape=1 + token_started=1 + ;; + *) + current+="$char" + token_started=1 + ;; + esac + done + + if [ "$escape" -eq 1 ]; then + current+="\\" + fi + + if [ -n "$quote" ]; then + echo "Invalid CUSTOM_LAUNCH_PARAMS: unmatched quote" >&2 + exit 1 + fi + + if [ "$token_started" -eq 1 ]; then + custom_params+=("$current") + fi + + params+=("${custom_params[@]}") +} + json_string() { local value="$1" @@ -125,4 +214,6 @@ add_param "-allowonlyplatform" "${ALLOW_ONLY_PLATFORM}" add_flag "-activateallcontent" "${ACTIVATE_ALL_CONTENT}" +append_custom_launch_params + echo "${params[@]}" diff --git a/tests/compile-parameters-server-config.sh b/tests/compile-parameters-server-config.sh index 4589416..e4f44da 100644 --- a/tests/compile-parameters-server-config.sh +++ b/tests/compile-parameters-server-config.sh @@ -58,6 +58,7 @@ reset_env() { export OVERRIDE_SERVER_CONFIG="" export MAX_NUMBER_PACKETS_SENT_PER_FRAME="" export NETWORK_SEND_RATE="" + export CUSTOM_LAUNCH_PARAMS="" mkdir -p "$STEAMAPPDIR" "$STEAMAPPDATADIR" } @@ -121,4 +122,15 @@ if contains_param "-serverconfig"; then fail "-serverconfig should not be emitted when OVERRIDE_SERVER_CONFIG=false" fi +reset_env +export CUSTOM_LAUNCH_PARAMS="-keepserverrunning -modflag simple-value -quoted \"value with spaces\" -single 'single value'" + +compile_parameters + +contains_param "-keepserverrunning" || fail "custom standalone flag was not appended" +contains_param_pair "-modflag" "simple-value" || fail "custom flag/value pair was not appended" +contains_param_pair "-quoted" "value with spaces" || fail "custom double-quoted value was not preserved" +contains_param_pair "-single" "single value" || fail "custom single-quoted value was not preserved" +contains_param_pair "-maxplayers" "10" || fail "existing parameters should still be emitted with custom launch params" + echo "compile-parameters-server-config: ok"