From 41c71fefafcc6615bd70c0ffbcf1d4bc730c1a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Carlos=20Fraqueiro=20da=20Palma?= Date: Sun, 7 Apr 2024 13:23:39 +0100 Subject: [PATCH] save-work.remote-pc --- .gitignore | 3 +- Dockerfile | 30 ++- default.env | 9 +- docker-compose.yml | 4 + docs/ENV_VARS.md | 19 +- docs/dev/DEV_GUIDE.md | 6 +- entrypoint.sh | 4 +- scripts/config/setup_configs.sh | 3 +- scripts/config/setup_palworld_settings_ini.sh | 7 + .../templates/PalWorldSettings.ini.template | 7 +- scripts/rcon/rconcli.sh | 7 +- scripts/server/start_server.sh | 16 +- scripts/server/stop_server.sh | 4 +- scripts/server_manager.sh | 4 +- scripts/utils/player_activity_monitor.sh | 218 +++--------------- src/custom_rcon_broadcast/go.mod | 5 - src/custom_rcon_broadcast/go.sum | 4 - src/custom_rcon_broadcast/main.go | 175 -------------- src/steamid64_to_palworlduid/go.mod | 8 + src/steamid64_to_palworlduid/go.sum | 4 + src/steamid64_to_palworlduid/main.go | 49 ++++ 21 files changed, 183 insertions(+), 403 deletions(-) delete mode 100644 src/custom_rcon_broadcast/go.mod delete mode 100644 src/custom_rcon_broadcast/go.sum delete mode 100644 src/custom_rcon_broadcast/main.go create mode 100644 src/steamid64_to_palworlduid/go.mod create mode 100644 src/steamid64_to_palworlduid/go.sum create mode 100644 src/steamid64_to_palworlduid/main.go 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) +}