diff --git a/.gitignore b/.gitignore
index c19ef03..d97b66c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ palworld/
test/
docker-compose-*.yml
custom.env
-test.env
\ No newline at end of file
+test.env
+scripts/api/*
diff --git a/Dockerfile b/Dockerfile
index b55a1ef..162f725 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
# Description: Dockerfile for Palworld Dedicated Server
-# Build the rcon binaries (GORCON and custom rcon broadcast built by @thejcpalma)
-FROM golang:1.22.0-bookworm as rcon-build
+# Build the rcon binary (GORCON) and the steam id 64 to palworld uid binary (by @thejcpalma)
+FROM golang:1.22.2-bookworm as golang-build
WORKDIR /build
@@ -19,11 +19,11 @@ RUN curl -fsSLO "$GORCON_RCONCLI_URL" \
&& rm -Rf "$GORCON_RCONCLI_DIR" \
&& go build -v ./cmd/gorcon
-WORKDIR /build/custom_rcon_broadcast/
+# Build the steam id 64 to palworld uid binary
+WORKDIR /build/steamid64_to_palworlduid_dir/
-# Build the custom rcon broadcast binary
-COPY /src/custom_rcon_broadcast/ .
-RUN go build -v -o /build/rcon_broadcast main.go
+COPY /src/steamid64_to_palworlduid/ .
+RUN go build -v -o /build/steamid64_to_palworlduid main.go
# Build the supercronic binary
FROM debian:bookworm-slim as supercronic-build
@@ -68,9 +68,9 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Copy the rcon, custom rcon broadcast (to fix spaces in the message) and supercronic binaries
-COPY --from=rcon-build --chmod=755 /build/gorcon /usr/local/bin/rcon
-COPY --from=rcon-build --chmod=755 /build/rcon_broadcast /usr/local/bin/rcon_broadcast
-COPY --from=supercronic-build --chmod=755 /usr/local/bin/supercronic /usr/local/bin/supercronic
+COPY --from=golang-build --chmod=755 /build/gorcon /usr/local/bin/rcon
+COPY --from=golang-build --chmod=755 /build/steamid64_to_palworlduid /usr/local/bin/steamid64_to_palworlduid
+COPY --from=supercronic-build --chmod=755 /usr/local/bin/supercronic /usr/local/bin/supercronic
ENV APP_ID=2394010
ENV SERVER_DIR=/home/steam/server
@@ -102,6 +102,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
GAME_CONFIG_PATH="/palworld/Pal/Saved/Config/LinuxServer" \
GAME_SETTINGS_FILE="/palworld/Pal/Saved/Config/LinuxServer/PalWorldSettings.ini" \
GAME_ENGINE_FILE="/palworld/Pal/Saved/Config/LinuxServer/Engine.ini" \
+ GAME_LOG_PATH="/palworld/logs" \
+ GAME_LOG_FILE="/palworld/logs/Palworld.log" \
BACKUP_PATH="/palworld/backups" \
STEAMCMD_PATH="/home/steam/steamcmd" \
RCON_CONFIG_FILE="/home/steam/server/configs/rcon.yaml" \
@@ -123,7 +125,6 @@ ENV DEBIAN_FRONTEND=noninteractive \
BACKUP_AUTO_CLEAN_AMOUNT_TO_KEEP=72 \
# Player monitoring settings
PLAYER_MONITOR_ENABLED=true \
- PLAYER_MONITOR_INTERVAL=60 \
# Webhook settings
WEBHOOK_ENABLED=false \
WEBHOOK_URL= \
@@ -246,10 +247,15 @@ ENV DEBIAN_FRONTEND=noninteractive \
REGION= \
USEAUTH=true \
BAN_LIST_URL=https://api.palworldgame.com/api/banlist.txt \
- SHOW_PLAYER_LIST=false
-
+ REST_API_ENABLED=false \
+ REST_API_PORT=8212 \
+ SHOW_PLAYER_LIST=false \
+ ALLOW_CONNECT_PLATFORM=Steam \
+ IS_USE_BACKUP_SAVE_DATA=false \
+ LOG_FORMAT_TYPE=json
EXPOSE 8211/udp
+EXPOSE 8212/tcp
EXPOSE 25575/tcp
VOLUME ["${GAME_ROOT}"]
diff --git a/default.env b/default.env
index 2cf03a0..4a90d9c 100644
--- a/default.env
+++ b/default.env
@@ -28,10 +28,8 @@ BACKUP_AUTO_CLEAN=true
# Keeps 3 days of backups with default value
BACKUP_AUTO_CLEAN_AMOUNT_TO_KEEP=72
-# Player monitoring settings (RCON needs to be enabled)
+# Player monitoring settings
PLAYER_MONITOR_ENABLED=true
-# Interval is in seconds
-PLAYER_MONITOR_INTERVAL=10
# Webhook-settings
# Use this guide colors: https://birdie0.github.io/discord-webhooks-guide/structure/embed/color.html
@@ -178,4 +176,9 @@ RCON_PORT=25575
REGION=
USEAUTH=true
BAN_LIST_URL=https://api.palworldgame.com/api/banlist.txt
+REST_API_ENABLED=true
+REST_API_PORT=8212
SHOW_PLAYER_LIST=false
+ALLOW_CONNECT_PLATFORM=Steam
+IS_USE_BACKUP_SAVE_DATA=false
+LOG_FORMAT_TYPE=Json
diff --git a/docker-compose.yml b/docker-compose.yml
index e8d4081..8f17ce1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,6 +10,10 @@ services:
published: 8211 # Gamerserver port on your host
protocol: udp
mode: host
+ - target: 8212 # REST API port inside of the container
+ published: 8212 # REST API port on your host
+ protocol: tcp
+ mode: host
- target: 25575 # RCON port inside of the container
published: 25575 # RCON port on your host
protocol: tcp
diff --git a/docs/ENV_VARS.md b/docs/ENV_VARS.md
index e65ce7f..f21978b 100644
--- a/docs/ENV_VARS.md
+++ b/docs/ENV_VARS.md
@@ -52,12 +52,14 @@ These are the overall settings for the dedicated server.
> [!IMPORTANT]
>
> **RCON** should be enabled for all for them to work as expected.
+> Player monitoring will still work without RCON but log format must be set to JSON.
These settings control the special features of the server:
- Auto updates:
- Auto restarts
- Auto backups
+ - Player monitoring
| Variable | Description | Default value | Allowed value |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------- | --------------------------------------- |
@@ -71,8 +73,7 @@ These settings control the special features of the server:
| `BACKUP_CRON_EXPRESSION` | The cron expression for the automatic backup function | `0 * * * *` | See [Cron expression](#cron-expression) |
| `BACKUP_AUTO_CLEAN` | Enables automatic cleanup of old backups | `true` | `false`/`true` |
| `BACKUP_AUTO_CLEAN_AMOUNT_TO_KEEP` | The amount of backups to keep | `72` | Integer |
-| `PLAYER_MONITOR_ENABLED` | Enables player monitoring for the server | `false` | `false`/`true` |
-| `PLAYER_MONITOR_INTERVAL` | The interval in seconds for the player monitoring (lower values means more impact on system resources) | `60` | Integer |
+| `PLAYER_MONITOR_ENABLED` | Enables player monitoring for the server | `true` | `false`/`true` |
#### Cron expression
@@ -178,7 +179,7 @@ Information sources and credits to the following websites:
| Variable | Game setting | Description | Default value | Allowed value |
| ------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------------- |
| `DIFFICULTY` | Difficulty | Choose one of the following:
`None`
`Normal`
`Difficult` | `None` | Enum |
-| `DAY_TIME_SPEED_RATE` | DayTimeSpeedRate | Day time speed - Smaller number means longer days | `1.000000` | Float |
+| `DAY_TIME_SPEED_RATE` | DayTimeSpeedRate | Day time speed - Smaller number means longer days | `1.000000` | Float |
| `NIGHT_TIME_SPEED_RATE` | NightTimeSpeedRate | Night time speed - Bigger number means shorter nights | `1.000000` | Float |
| `EXP_RATE` | ExpRate | EXP rate | `1.000000` | Float |
| `PAL_CAPTURE_RATE` | PalCaptureRate | Pal capture rate | `1.000000` | Float |
@@ -228,19 +229,23 @@ Information sources and credits to the following websites:
| `ENABLE_DEFENSE_OTHER_GUILD_PLAYER` | bEnableDefenseOtherGuildPlayer | Allows defense against other guild players | `false` | `false`/`true` |
| `COOP_PLAYER_MAX_NUM` | CoopPlayerMaxNum | Maximum number of players in a guild | `4` | Integer |
| `MAX_PLAYERS` | ServerPlayerMaxNum | Maximum number of people who can join the server | `32` | Integer |
-| `SERVER_NAME` | ServerName | Server name | `thejcpalma-docker-generated-###RANDOM###` | Integer |
+| `SERVER_NAME` | ServerName | Server name | `thejcpalma-docker-generated-###RANDOM###` | String |
| `SERVER_DESCRIPTION` | ServerDescription | Server description | `Palworld Dedicated Server running in Docker by thejcpalma` | String |
| `ADMIN_PASSWORD` | AdminPassword | Set the server admin password. | `adminPasswordHere` | String |
| `SERVER_PASSWORD` | ServerPassword | Set the server password. | `serverPasswordHere` | String |
-| `PUBLIC_PORT` | PublicPort | Public port number | `8211` | Integer |
+| `PUBLIC_PORT` | PublicPort | Public port number | `8211` | `1024`-`65535` |
| `PUBLIC_IP` | PublicIP | Public IP or FQDN | | String |
| `RCON_ENABLED` | RCONEnabled | Enable RCON - Use ADMIN_PASSWORD to login | `false` | `false`/`true` |
-| `RCON_PORT` | RCONPort | Port number for RCON | `25575` | Integer |
+| `RCON_PORT` | RCONPort | Port number for RCON | `25575` | `1024`-`65535` |
| `REGION` | Region | Area | | String |
| `USEAUTH` | bUseAuth | Use authentication | `true` | `false`/`true` |
| `BAN_LIST_URL` | BanListURL | Which ban list to use | `https://api.palworldgame.com/api/banlist.txt` | String |
+| `REST_API_ENABLED` | RESTAPIEnabled | Enable REST API for the palworld server | `false` | `false`/`true` |
+| `REST_API_PORT` | RESTAPIPort | REST API port to connect to | `8212` | `1024`-`65535` |
| `SHOW_PLAYER_LIST` | bShowPlayerList | Make the player list public on a community server | `false` | `false`/`true` |
-
+| `ALLOW_CONNECT_PLATFORM` | AllowConnectPlatform | !!Doesn't work this version!! | `Steam` | Unknown values |
+| `IS_USE_BACKUP_SAVE_DATA` | bIsUseBackupSaveData | Enable world backup | `true` | `false`/`true` |
+| `LOG_FORMAT_TYPE` | LogFormatType | Log format Text or Json | `Text` | `Text`/`Json` |
## Webhook Settings
diff --git a/docs/dev/DEV_GUIDE.md b/docs/dev/DEV_GUIDE.md
index 0e77b08..e5ec93f 100644
--- a/docs/dev/DEV_GUIDE.md
+++ b/docs/dev/DEV_GUIDE.md
@@ -22,7 +22,7 @@ services:
palworld-dedicated-server:
build: .
container_name: palworld-dedicated-server
- image: thejcpalma/palworld-dedicated-server:latest
+ image: palworld-dedicated-server:latest
restart: unless-stopped
stop_grace_period: 30s # Set to however long you are willing to wait for the container to gracefully stop
ports:
@@ -30,6 +30,10 @@ services:
published: 8211 # Gamerserver port on your host
protocol: udp
mode: host
+ - target: 8212 # REST API port inside of the container
+ published: 8212 # REST API port on your host
+ protocol: tcp
+ mode: host
- target: 25575 # RCON port inside of the container
published: 25575 # RCON port on your host
protocol: tcp
diff --git a/entrypoint.sh b/entrypoint.sh
index 85abcb5..e575bf0 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -17,7 +17,9 @@ elif [[ "$(id -u steam)" -ne "${PUID}" ]] || [[ "$(id -g steam)" -ne "${PGID}" ]
groupmod -g "${PGID}" steam && usermod -u "${PUID}" -g "${PGID}" steam
fi
-mkdir -p /palworld/backups
+# Create the necessary directories
+mkdir -p ${GAME_LOG_PATH}
+mkdir -p ${BACKUP_PATH}
chown -R steam:steam /palworld /home/steam/
# shellcheck disable=SC2317
diff --git a/scripts/config/setup_configs.sh b/scripts/config/setup_configs.sh
index 8966090..e6fe1d2 100644
--- a/scripts/config/setup_configs.sh
+++ b/scripts/config/setup_configs.sh
@@ -17,7 +17,8 @@ function setup_configs() {
log_warning ">> 'SERVER_SETTINGS_MODE' is set to '${SERVER_SETTINGS_MODE}', environment variables NOT used to configure the server!"
# Copy default-config, which comes with SteamCMD to gameserver save location
- cp "${GAME_ROOT}/DefaultPalWorldSettings.ini" "${GAME_SETTINGS_FILE}"
+ # -n option will not overwrite an already existing file
+ cp -n "${GAME_ROOT}/DefaultPalWorldSettings.ini" "${GAME_SETTINGS_FILE}"
log_warning ">> File '${GAME_ENGINE_FILE}' has to be manually set by user."
log_warning ">> File '${GAME_SETTINGS_FILE}' has to be manually set by user."
diff --git a/scripts/config/setup_palworld_settings_ini.sh b/scripts/config/setup_palworld_settings_ini.sh
index 6ee1821..47d0cc9 100644
--- a/scripts/config/setup_palworld_settings_ini.sh
+++ b/scripts/config/setup_palworld_settings_ini.sh
@@ -80,7 +80,14 @@ function setup_palworld_settings_ini(){
check_and_export "other" "Region" "${REGION}" ""
check_and_export "bool" "bUseAuth" "${USE_AUTH}" "true"
check_and_export "other" "BanListURL" "${BAN_LIST_URL}" ""
+ check_and_export "bool" "RESTAPIEnabled" "${REST_API_ENABLED}" "false"
+ check_and_export "int" "RESTAPIPort" "${REST_API_PORT}" "8212"
check_and_export "bool" "bShowPlayerList" "${SHOW_PLAYER_LIST}" "true"
+ allow_connect_platform_options=("Steam")
+ check_and_export "list" "AllowConnectPlatform" "${ALLOW_CONNECT_PLATFORM}" "Steam" "${allow_connect_platform_options[@]}"
+ check_and_export "bool" "bIsUseBackupSaveData" "${IS_USE_BACKUP_SAVE_DATA}" "true"
+ log_format_type_options=("json" "text")
+ check_and_export "list" "LogFormatType" "${LOG_FORMAT_TYPE}" "json" "${log_format_type_options[@]}"
envsubst < "${GAME_SETTINGS_FILE}.tmp" > "${GAME_SETTINGS_FILE}" && rm "${GAME_SETTINGS_FILE}.tmp"
diff --git a/scripts/config/templates/PalWorldSettings.ini.template b/scripts/config/templates/PalWorldSettings.ini.template
index fd693a7..8fb96c9 100755
--- a/scripts/config/templates/PalWorldSettings.ini.template
+++ b/scripts/config/templates/PalWorldSettings.ini.template
@@ -62,5 +62,10 @@ RCONPort=${RCONPort},
Region=${Region},
bUseAuth=${bUseAuth},
BanListURL=${BanListURL},
-bShowPlayerList=${bShowPlayerList}
+RESTAPIEnabled=${RESTAPIEnabled},
+RESTAPIPort=${RESTAPIPort},
+bShowPlayerList=${bShowPlayerList},
+AllowConnectPlatform=${AllowConnectPlatform},
+bIsUseBackupSaveData=${bIsUseBackupSaveData},
+LogFormatType=${LogFormatType}
)
diff --git a/scripts/rcon/rconcli.sh b/scripts/rcon/rconcli.sh
index 39b9625..5087f0d 100644
--- a/scripts/rcon/rconcli.sh
+++ b/scripts/rcon/rconcli.sh
@@ -19,10 +19,11 @@ rconcli() {
return
fi
- # Edge case for broadcast because it doesn't support spaces in the message
if [[ ${cmd,,} == broadcast* ]]; then
- cmd=${cmd#broadcast } # Remove 'broadcast ' from the command (also removes the space after 'broadcast')
- output=$(rcon_broadcast -c "${RCON_CONFIG_FILE}" "${cmd}" | tr -d '\0')
+ output=$(rcon -c "${RCON_CONFIG_FILE}" "${cmd}" | tr -d '\0')
+ if [[ ${output} == Broadcasted:* ]]; then
+ output="Broadcasted: ${cmd#broadcast }"
+ fi
else
output=$(rcon -c "${RCON_CONFIG_FILE}" "${cmd}" | tr -d '\0')
fi
diff --git a/scripts/server/start_server.sh b/scripts/server/start_server.sh
index 218761e..b708ee8 100644
--- a/scripts/server/start_server.sh
+++ b/scripts/server/start_server.sh
@@ -27,12 +27,22 @@ function start_server() {
START_OPTIONS+=("-RCONPort=${RCON_PORT}")
fi
+ START_OPTIONS+=("-logformat=json")
+
send_start_notification
- # Start the player activity monitor with a delay
- (sleep 5 && start_player_activity_monitor) &
+ timestamp=$(date +%Y%m%d%H%M%S)
+
+ if [ -f "${GAME_LOG_FILE}" ]; then
+ mv "${GAME_LOG_FILE}" "${GAME_LOG_PATH}/${timestamp}_Palworld.log"
+ fi
+
+ touch "${GAME_LOG_FILE}"
+
+ # Start the player activity monitor
+ start_player_activity_monitor &
- ./PalServer.sh "${START_OPTIONS[@]}"
+ ./PalServer.sh "${START_OPTIONS[@]}" | tee -a "${GAME_LOG_FILE}"
popd > /dev/null || exit
}
diff --git a/scripts/server/stop_server.sh b/scripts/server/stop_server.sh
index 93c120b..1ce1957 100644
--- a/scripts/server/stop_server.sh
+++ b/scripts/server/stop_server.sh
@@ -30,8 +30,8 @@ function stop_server() {
if [[ -n ${RCON_ENABLED} ]] && [[ ${RCON_ENABLED,,} == "true" ]]; then
rcon_save_and_shutdown
else
- kill -SIGTERM "$(pidof PalServer-Linux-Test)"
- tail --pid="$(pidof PalServer-Linux-Test)" -f 2>/dev/null
+ kill -SIGTERM "$(pidof PalServer-Linux-Shipping)"
+ tail --pid="$(pidof PalServer-Linux-Shipping)" -f 2>/dev/null
fi
send_stop_notification
diff --git a/scripts/server_manager.sh b/scripts/server_manager.sh
index cddddd2..3335f8e 100644
--- a/scripts/server_manager.sh
+++ b/scripts/server_manager.sh
@@ -40,12 +40,12 @@ function parse_arguments() {
# Evaluate the command
case "$1" in
init)
- check_default_credentials
-
install_server
update_server
setup_configs
+ check_default_credentials
+
setup_crons
start_server
diff --git a/scripts/utils/player_activity_monitor.sh b/scripts/utils/player_activity_monitor.sh
index 2f51c9e..507a378 100755
--- a/scripts/utils/player_activity_monitor.sh
+++ b/scripts/utils/player_activity_monitor.sh
@@ -2,206 +2,60 @@
# shellcheck source=/dev/null
source "${SERVER_DIR}"/scripts/utils/logs.sh
-source "${SERVER_DIR}"/scripts/rcon/aliases.sh
source "${SERVER_DIR}"/scripts/webhook/aliases.sh
-# Variables to store the current and previous player lists
-declare -a current_players
-declare -a previous_players
+function log_player_join() {
+ local player_name=$1
+ local player_uid=$2
+ local player_steam_uid=$3
-# An issue arises when players have special characters, commas or spaces in their name.
-# The script uses commas to split the player information into separate fields.
-# If a player's name contains a comma, it will be incorrectly split into separate fields.
-# The same issue occurs with spaces when checking for players who have joined or left.
-#
-# To fix this, we assume that the player's name is everything before the last
-# two commas (which separate the name, player UID, and Steam UID),
-# and that the player UID and Steam UID will never contain any commas.
-#
-# To handle names with multibyte characters, we use 'awk'.
-# 'awk' is used to split the lines on commas and extract the player UID (the second-to-last field)
-# and the player name (all fields before the last two).
-# This ensures that we always split on the last two commas.
-# This approach correctly handles player names that contain commas, spaces, and multibyte characters.
-#
-# Special chars also break RCON usage when fecthing Steam UDIs
-# We get a Steam UID with 16 characters, but we need 17 to be valid
-# We can add a number at the end of the Steam UID to get a valid one
-# but we don't have a way to know which one is the correct
+ log_info -n "> Player joined: " && log_base -n "'$player_name'" && \
+ log_info -n " | UID: " && log_base -n "$player_uid" && \
+ log_info -n " | Steam ID: " && log_base "$player_steam_uid"
-# Format: name,playeruid,steamid
-# Function to get the current player list
-get_current_players() {
- # Clear the current players array
- current_players=()
-
- # Execute the rcon command to get the list of players (supress stderr)
- player_list=$(rcon_get_player_list 2>/dev/null)
-
- # Remove the first line (header) from the player list
- player_list=$(echo "$player_list" | tail -n +2)
-
- # Convert the player list to an array
- while IFS= read -r line; do
- # Skip lines that are empty or contain only commas
- if [[ "$line" =~ ^,*$ ]]; then
- continue
- fi
- # Skip lines that contain the UID '00000000'
- if [[ "$line" =~ ,00000000, ]]; then
- continue
- fi
- current_players+=("$line")
- done <<< "$player_list"
-}
-
-check_steam_profile() {
- local clean_output=false
- if [ "$1" = "clean" ]; then
- clean_output=true
- shift
- fi
-
- link="https://steamcommunity.com/profiles/$1"
- content=$(curl -sL "${link}")
- if echo "$content" | grep -q 'This user has not yet set up their Steam Community profile.'; then
- return
- else
- profile_name=$(echo "$content" | grep -oPm 1 '(?<=).*(?=)')
- if [[ $clean_output = true ]]; then
- echo "- [${profile_name}](<${link}>)"
- else
- log_info -n "> Profile name is: " && log_base -n "${profile_name}" && log_info -n " | Profile link: " && log_base "${link}"
- fi
- fi
+ send_player_join_notification "\`$player_name\`" "$player_uid" "$player_steam_uid"
}
-# Function to compare the current player list with the previous one
-compare_players() {
- # Arrays to hold the players who have joined and left
- local joined_players=()
- local left_players=()
+function log_player_left() {
+ local player_name=$1
+ local player_uid=$2
+ local player_steam_uid=$3
- # Convert the player lists to arrays of playeruids
- mapfile -t previous_uids < <(printf "%s\n" "${previous_players[@]}" | awk -F',' '{if (NF>1) print $(NF-1)}')
- mapfile -t current_uids < <(printf "%s\n" "${current_players[@]}" | awk -F',' '{if (NF>1) print $(NF-1)}')
+ log_info -n "> Player left: " && log_base -n "'$player_name'" && \
+ log_info -n " | UID: " && log_base -n "$player_uid" && \
+ log_info -n " | Steam ID: " && log_base "$player_steam_uid"
- # Check for players who have left
- for i in "${!previous_uids[@]}"; do
- if ! [[ " ${current_uids[*]} " =~ ${previous_uids[i]} ]]; then
- left_players+=("${previous_players[i]}")
- fi
- done
-
- # Check for players who have joined
- for i in "${!current_uids[@]}"; do
- if ! [[ " ${previous_uids[*]} " =~ ${current_uids[i]} ]]; then
- joined_players+=("${current_players[i]}")
- fi
- done
-
- # Log each player who has joined
- if [ ${#joined_players[@]} -ne 0 ]; then
- for player in "${joined_players[@]}"; do
- local player_name
- local player_uid
- local player_steam_uid
- local possible_steam_ids
-
- player_name=$(echo "$player" | awk 'BEGIN{FS=OFS=","} {NF-=2; print $0}' | sed 's/,*$//')
- player_uid=$(echo "$player" | awk 'BEGIN{FS=OFS=","} {print $(NF-1)}')
- player_steam_uid=$(echo "$player" | awk 'BEGIN{FS=OFS=","} {print $NF}')
-
- log_info -n "> Player joined: " && log_base -n "'$player_name' " && \
- log_info -n "| UID: " && log_base -n "$player_uid" && \
- log_info -n "| Steam ID: " && log_base "$player_steam_uid"
-
- if [ "${#player_steam_uid}" -ne 17 ]; then
- log_warning ">> Invalid Steam ID (Player name has special characters!) - Should have 17 digits but has ${#player_steam_uid} digits!"
- log_warning ">> Possible Steam IDs:"
- for i in {0..9}; do
- result=$(check_steam_profile "${player_steam_uid}${i}")
- if [[ -n "$result" ]]; then
- echo "$result"
- possible_steam_ids+="$(check_steam_profile "clean" "${player_steam_uid}${i}")\n"
- fi
- done
- player_steam_uid="###INVALID_STEAM_UID###"
- fi
- player_name=$(echo "$player" | awk 'BEGIN{FS=OFS=","} {NF-=2; print $0}' | sed 's/,*$//' | tr '`' "'" | sed 's/\\\\/\\\\\\\\/g')
- send_player_join_notification "\`$player_name\`" "$player_uid" "$player_steam_uid" "$possible_steam_ids"
- done
- fi
-
- # Log each player who has left
- if [ ${#left_players[@]} -ne 0 ]; then
- for player in "${left_players[@]}"; do
- local player_name
- local player_uid
- local player_steam_uid
- local possible_steam_ids
-
- player_name=$(echo "$player" | awk 'BEGIN{FS=OFS=","} {NF-=2; print $0}' | sed 's/,*$//')
- player_uid=$(echo "$player" | awk 'BEGIN{FS=OFS=","} {print $(NF-1)}')
- player_steam_uid=$(echo "$player" | awk 'BEGIN{FS=OFS=","} {print $NF}')
-
- log_info -n "> Player left: " && log_base -n "'$player_name' " && \
- log_info -n "| UID: " && log_base -n "$player_uid" && \
- log_info -n "| Steam ID: " && log_base "$player_steam_uid"
-
- if [ "${#player_steam_uid}" -ne 17 ]; then
- log_warning ">> Invalid Steam ID (Player name has special characters!) - Should have 17 digits but has ${#player_steam_uid} digits!"
- log_warning ">> Possible Steam IDs:"
- for i in {0..9}; do
- result=$(check_steam_profile "${player_steam_uid}${i}")
- if [[ -n "$result" ]]; then
- echo "$result"
- possible_steam_ids+="$(check_steam_profile "clean" "${player_steam_uid}${i}")\n"
- fi
- done
- player_steam_uid="###INVALID_STEAM_UID###"
- fi
- player_name=$(echo "$player" | awk 'BEGIN{FS=OFS=","} {NF-=2; print $0}' | sed 's/,*$//' | tr '`' "'" | sed 's/\\\\/\\\\\\\\/g')
- send_player_leave_notification "\`$player_name\`" "$player_uid" "$player_steam_uid" "$possible_steam_ids"
- done
- fi
-
- # Replace the previous player list with the current player list
- previous_players=("${current_players[@]}")
+ send_player_join_notification "\`$player_name\`" "$player_uid" "$player_steam_uid"
}
function start_player_activity_monitor() {
- if [ "${RCON_ENABLED,,}" != true ]; then
- log_error ">>> RCON is not enabled. Player monitoring only works with RCON! Aborting..."
- exit 1
- fi
-
if [[ "${PLAYER_MONITOR_ENABLED,,}" != "true" ]]; then
log_warning ">> Player monitor is disabled."
exit 1
fi
- if ! [[ "${PLAYER_MONITOR_INTERVAL}" =~ ^[1-9][0-9]*$ ]] || [ "${PLAYER_MONITOR_INTERVAL}" -lt 1 ]; then
- log_error ">>> Invalid 'PLAYER_MONITOR_INTERVAL' value: '${PLAYER_MONITOR_INTERVAL}' . Please provide a positive integer. Aborting..."
- exit 1
- fi
-
log_success ">>> Player activity monitor started"
- # Main loop
- while true; do
- # Get the current player list
- get_current_players
-
- # Compare it with the previous one
- compare_players
-
- # Move the current player list to the previous player list
- previous_players=("${current_players[@]}")
-
- # Wait for a while before the next iteration
- sleep "${PLAYER_MONITOR_INTERVAL}"
+ tail -f "${GAME_LOG_FILE}" | while read -r LOGLINE
+ do
+ # Check if the line is valid JSON and has an 'event' field
+ if jq -e '.event' <<< "${LOGLINE}" > /dev/null 2>&1; then
+ # process the line
+ event=$(jq -r '.event' <<< "${LOGLINE}")
+ if [[ "$event" == "join" ]]; then
+ player_name=$(jq -r '.playername' <<< "${LOGLINE}")
+ steam_id=$(jq -r '.userid' <<< "${LOGLINE}" | sed 's/^steam_//')
+ user_id=$(steamid64_to_palworlduid "${steam_id}")
+
+ log_player_join "${player_name}" "${user_id}" "${steam_id}"
+ elif [[ "$event" == "left" ]]; then
+ player_name=$(jq -r '.playername' <<< "${LOGLINE}")
+ steam_id=$(jq -r '.userid' <<< "${LOGLINE}" | sed 's/^steam_//')
+ user_id=$(steamid64_to_palworlduid "${steam_id}")
+
+ log_player_left "${player_name}" "${user_id}" "${steam_id}"
+ fi
+ fi
done
-
}
diff --git a/src/custom_rcon_broadcast/go.mod b/src/custom_rcon_broadcast/go.mod
deleted file mode 100644
index 50e07d0..0000000
--- a/src/custom_rcon_broadcast/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module custom_rcon_broadcast
-
-go 1.22
-
-require gopkg.in/yaml.v2 v2.4.0
diff --git a/src/custom_rcon_broadcast/go.sum b/src/custom_rcon_broadcast/go.sum
deleted file mode 100644
index dd0bc19..0000000
--- a/src/custom_rcon_broadcast/go.sum
+++ /dev/null
@@ -1,4 +0,0 @@
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/src/custom_rcon_broadcast/main.go b/src/custom_rcon_broadcast/main.go
deleted file mode 100644
index 4a39d1b..0000000
--- a/src/custom_rcon_broadcast/main.go
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- Author: João Palma
- Project Name: thejcpalma/palworld-dedicated-server-docker
- GitHub: https://github.com/thejcpalma/palworld-dedicated-server-docker
- DockerHub: https://hub.docker.com/r/thejcpalma/palworld-dedicated-server
-*/
-
-package main
-
-import (
- "bytes"
- "encoding/binary"
- "flag"
- "fmt"
- "os"
- "log"
- "net"
- "strconv"
- "strings"
- "gopkg.in/yaml.v2"
-)
-
-// ServerConfig is a structure that holds the configuration details for the RCON server.
-// It is used to unmarshal the YAML configuration file provided by the user.
-// The struct has one field, Default, which is an anonymous struct.
-//
-// The Default struct has two fields:
-// Address: This is a string that holds the IP address and port of the RCON server.
-// It is expected to be in the format "ip:port".
-// Password: This is a string that holds the password for the RCON server.
-//
-// The yaml tags are used to map the struct fields to the corresponding keys in the YAML file.
-type ServerConfig struct {
- Default struct {
- Address string `yaml:"address"` // The IP address and port of the RCON server
- Password string `yaml:"password"` // The password for the RCON server
- } `yaml:"default"` // The default configuration for the RCON server
-}
-
-
-// sendRconCommand sends a command to a RCON server and returns the server's response.
-// It establishes a TCP connection to the server, authenticates using the provided password,
-// sends the command, and reads the server's response.
-// sendRconCommand sends an RCON command to a server and returns the server's response.
-// It establishes a TCP connection to the RCON server using the provided IP address and port.
-// The authentication packet is created with the provided password and sent to the server.
-// If the authentication is successful, a command packet is created with the provided command and sent to the server.
-// The server's response to the command packet is read and returned as a string.
-// If any error occurs during the process, an error is returned.
-func sendRconCommand(ip string, port int, password string, command string) (string, error) {
- // Establish a TCP connection to the RCON server.
- conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, port))
- if err != nil {
- return "", err
- }
- defer conn.Close()
-
- // Create an authentication packet.
- // The packet consists of a little-endian int32 length field, a little-endian int32 request ID field,
- // a little-endian int32 type field, the password as a null-terminated string, and two null bytes.
- authPacket := new(bytes.Buffer)
- binary.Write(authPacket, binary.LittleEndian, int32(10+len(password))) // Length
- binary.Write(authPacket, binary.LittleEndian, int32(1)) // Request ID
- binary.Write(authPacket, binary.LittleEndian, int32(3)) // Type
- authPacket.WriteString(password) // Password
- authPacket.Write([]byte{0, 0}) // Null bytes
-
- // Send the authentication packet to the server.
- _, err = conn.Write(authPacket.Bytes())
- if err != nil {
- return "", err
- }
-
- // Read the server's response to the authentication packet.
- authResponse := make([]byte, 4096)
- _, err = conn.Read(authResponse)
- if err != nil {
- return "", err
- }
-
- // Check the server's response to the authentication packet.
- // If the response ID is -1, the authentication failed.
- responseID := int32(binary.LittleEndian.Uint32(authResponse[4:8]))
- if responseID == -1 {
- return "", fmt.Errorf("authentication failed")
- }
-
- // Create a command packet.
- // The packet consists of a little-endian int32 length field, a little-endian int32 request ID field,
- // a little-endian int32 type field, the command as a null-terminated string, and two null bytes.
- commandBytes := append([]byte(command), 0, 0)
- commandPacket := new(bytes.Buffer)
- binary.Write(commandPacket, binary.LittleEndian, int32(10+len(commandBytes)-2)) // Length
- binary.Write(commandPacket, binary.LittleEndian, int32(2)) // Request ID
- binary.Write(commandPacket, binary.LittleEndian, int32(2)) // Type
- commandPacket.Write(commandBytes) // Command
-
- // Send the command packet to the server.
- _, err = conn.Write(commandPacket.Bytes())
- if err != nil {
- return "", err
- }
-
- // Read the server's response to the command packet.
- responsePacket := make([]byte, 4096)
- _, err = conn.Read(responsePacket)
- if err != nil {
- return "", err
- }
-
- // Extract the body of the server's response from the response packet.
- responseBody := string(responsePacket[12 : len(responsePacket)-2])
-
- return responseBody, nil
-}
-
-
-// main is the entry point of the program.
-// It parses command-line flags, reads a configuration file, sends the 'broadcast' command to the Palworld RCON server,
-// and prints the server's response.
-func main() {
- // Parse command-line flags
- configPath := flag.String("c", "", "Path to the YAML configuration file")
- flag.Parse()
-
- // Check if a configuration file was provided
- if *configPath == "" {
- log.Fatal("Please provide a configuration file with the -c flag.")
- }
-
- // Read the configuration file
- configData, err := os.ReadFile(*configPath)
- if err != nil {
- log.Fatal(err)
- }
-
- // Parse the configuration file
- var config ServerConfig
- err = yaml.Unmarshal(configData, &config)
- if err != nil {
- log.Fatal(err)
- }
-
- // Split the address into IP and port
- splitAddress := strings.Split(config.Default.Address, ":")
- ip := splitAddress[0]
- port, err := strconv.Atoi(splitAddress[1])
- if err != nil {
- log.Fatal(err)
- }
-
- // Check if a message was provided
- if len(flag.Args()) < 1 {
- log.Fatal("Please provide a message as a command-line argument.")
- }
-
- // Get the message from the command-line arguments
- message := flag.Args()[0]
- modded_message := strings.ReplaceAll(message, " ", "\xa0") // Replace spaces with non-breaking spaces
- command := "broadcast " + modded_message // Construct the command
-
- // Send the command to the RCON server and print the server's response.
- result, err := sendRconCommand(ip, port, config.Default.Password, command)
- if err != nil {
- log.Fatal(err)
- }
-
- // Check if the server's response indicates that the message was broadcasted
- if strings.HasPrefix(result, "Broadcasted:") {
- // Prints the message that was broadcasted
- fmt.Println("Broadcasted: " + message)
- } else {
- fmt.Println("Error on broadcast!")
- }
-}
diff --git a/src/steamid64_to_palworlduid/go.mod b/src/steamid64_to_palworlduid/go.mod
new file mode 100644
index 0000000..11a549e
--- /dev/null
+++ b/src/steamid64_to_palworlduid/go.mod
@@ -0,0 +1,8 @@
+module steamid64_to_palworlduid
+
+go 1.22.2
+
+require (
+ github.com/zhenjl/cityhash v0.0.0-20131128155616-cdd6a94144ab
+ golang.org/x/text v0.14.0
+)
diff --git a/src/steamid64_to_palworlduid/go.sum b/src/steamid64_to_palworlduid/go.sum
new file mode 100644
index 0000000..e096414
--- /dev/null
+++ b/src/steamid64_to_palworlduid/go.sum
@@ -0,0 +1,4 @@
+github.com/zhenjl/cityhash v0.0.0-20131128155616-cdd6a94144ab h1:BWHvAOZz0pBILkGl/ebPQKZDrqbaWj/iN9RE8AvaTvg=
+github.com/zhenjl/cityhash v0.0.0-20131128155616-cdd6a94144ab/go.mod h1:P6L88wrqK99Njntah9SB7AyzFpUXsXYq06LkjixxQmY=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
diff --git a/src/steamid64_to_palworlduid/main.go b/src/steamid64_to_palworlduid/main.go
new file mode 100644
index 0000000..27b0035
--- /dev/null
+++ b/src/steamid64_to_palworlduid/main.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "golang.org/x/text/encoding/unicode"
+ "golang.org/x/text/transform"
+ "github.com/zhenjl/cityhash"
+)
+
+
+func steamIDToPlayerUID(steamID int) (int) {
+
+ steamIDStr := strconv.Itoa(steamID)
+
+ utf16Encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
+ steamIDUtf16, _, _ := transform.Bytes(utf16Encoder, []byte(steamIDStr))
+
+ hash := cityhash.CityHash64(steamIDUtf16, uint32(len(steamIDUtf16)))
+
+ low := u32(hash)
+ high := u32(hash >> 32)
+
+ playerUID := low + high*23
+
+ return int(playerUID)
+}
+
+func u32(value uint64) uint32 {
+ return uint32(value & 0xFFFFFFFF)
+}
+
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Println("Please provide a Steam ID as an argument.")
+ return
+ }
+
+ steamID, err := strconv.Atoi(os.Args[1])
+ if err != nil {
+ fmt.Println("Invalid Steam ID. Please provide a numeric Steam ID.")
+ return
+ }
+
+ playerUID := steamIDToPlayerUID(steamID)
+ fmt.Println(playerUID)
+}