diff --git a/Dockerfile b/Dockerfile index 6d4183626..2f05607f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ gettext-base=0.21-4 \ xdg-user-dirs=0.17-2 \ jo=1.3-2 \ + jq=1.6-2.1 \ + python3.9=3.9.2-1 \ + python3-pip=20.3.4-4+deb11u1 \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && pip3 install --force-reinstall --no-cache-dir "palworld-save-tools==0.18.0" # install rcon and supercronic SHELL ["/bin/bash", "-o", "pipefail", "-c"] diff --git a/README.md b/README.md index 37b99cd4e..56c1d183f 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,7 @@ It is highly recommended you set the following environment values before startin | SERVER_PASSWORD | Secure your community server with a password | | "string" | | ADMIN_PASSWORD | Secure administration access in the server with a password | | "string" | | UPDATE_ON_BOOT** | Update/Install the server when the docker container starts (THIS HAS TO BE ENABLED THE FIRST TIME YOU RUN THE CONTAINER) | true | true/false | +| GENERATE_WORLD_OPTION | Generate a WorldOption.sav file based on the compiled `PalWorldSettings.ini` (THIS WILL FORCE THE SERVER TO USE THE `WorldOption.sav` FILE FOR CONFIGURATIONS AND IGNORE `PalWorldSettings.ini`) | false | true/false | | RCON_ENABLED*** | Enable RCON for the Palworld server | true | true/false | | RCON_PORT | RCON port to connect to | 25575 | 1024-65535 | | QUERY_PORT | Query port used to communicate with Steam servers | 27015 | 1024-65535 | diff --git a/scripts/files/WorldOption.json.template b/scripts/files/WorldOption.json.template new file mode 100644 index 000000000..5c3a00c40 --- /dev/null +++ b/scripts/files/WorldOption.json.template @@ -0,0 +1,316 @@ +{ + "header": { + "magic": 1396790855, + "save_game_version": 3, + "package_file_version_ue4": 522, + "package_file_version_ue5": 1008, + "engine_version_major": 5, + "engine_version_minor": 1, + "engine_version_patch": 1, + "engine_version_changelist": 0, + "engine_version_branch": "++UE5+Release-5.1", + "custom_version_format": 3, + "custom_versions": [ + [ + "40d2fba7-4b48-4ce5-b038-5a75884e499e", + 7 + ], + [ + "fcf57afa-5076-4283-b9a9-e658ffa02d32", + 76 + ], + [ + "0925477b-763d-4001-9d91-d6730b75b411", + 1 + ], + [ + "4288211b-4548-16c6-1a76-67b2507a2a00", + 1 + ], + [ + "1ab9cecc-0000-6913-0000-4875203d51fb", + 100 + ], + [ + "4cef9221-470e-d43a-7e60-3d8c16995726", + 1 + ], + [ + "e2717c7e-52f5-44d3-950c-5340b315035e", + 7 + ], + [ + "11310aed-2e55-4d61-af67-9aa3c5a1082c", + 17 + ], + [ + "a7820cfb-20a7-4359-8c54-2c149623cf50", + 21 + ], + [ + "f6dfbb78-bb50-a0e4-4018-b84d60cbaf23", + 2 + ], + [ + "24bb7af3-5646-4f83-1f2f-2dc249ad96ff", + 5 + ], + [ + "76a52329-0923-45b5-98ae-d841cf2f6ad8", + 5 + ], + [ + "5fbc6907-55c8-40ae-8e67-f1845efff13f", + 1 + ], + [ + "82e77c4e-3323-43a5-b46b-13c597310df3", + 0 + ], + [ + "0ffcf66c-1190-4899-b160-9cf84a46475e", + 1 + ], + [ + "9c54d522-a826-4fbe-9421-074661b482d0", + 44 + ], + [ + "b0d832e4-1f89-4f0d-accf-7eb736fd4aa2", + 10 + ], + [ + "e1c64328-a22c-4d53-a36c-8e866417bd8c", + 0 + ], + [ + "375ec13c-06e4-48fb-b500-84f0262a717e", + 4 + ], + [ + "e4b068ed-f494-42e9-a231-da0b2e46bb41", + 40 + ], + [ + "cffc743f-43b0-4480-9391-14df171d2073", + 37 + ], + [ + "b02b49b5-bb20-44e9-a304-32b752e40360", + 3 + ], + [ + "a4e4105c-59a1-49b5-a7c5-40c4547edfee", + 0 + ], + [ + "39c831c9-5ae6-47dc-9a44-9c173e1c8e7c", + 0 + ], + [ + "78f01b33-ebea-4f98-b9b4-84eaccb95aa2", + 20 + ], + [ + "6631380f-2d4d-43e0-8009-cf276956a95a", + 0 + ], + [ + "12f88b9f-8875-4afc-a67c-d90c383abd29", + 45 + ], + [ + "7b5ae74c-d270-4c10-a958-57980b212a5a", + 13 + ], + [ + "d7296918-1dd6-4bdd-9de2-64a83cc13884", + 3 + ], + [ + "c2a15278-bfe7-4afe-6c17-90ff531df755", + 1 + ], + [ + "6eaca3d4-40ec-4cc1-b786-8bed09428fc5", + 3 + ], + [ + "29e575dd-e0a3-4627-9d10-d276232cdcea", + 17 + ], + [ + "af43a65d-7fd3-4947-9873-3e8ed9c1bb05", + 15 + ], + [ + "6b266cec-1ec7-4b8f-a30b-e4d90942fc07", + 1 + ], + [ + "0df73d61-a23f-47ea-b727-89e90c41499a", + 1 + ], + [ + "601d1886-ac64-4f84-aa16-d3de0deac7d6", + 80 + ], + [ + "5b4c06b7-2463-4af8-805b-bf70cdf5d0dd", + 10 + ], + [ + "e7086368-6b23-4c58-8439-1b7016265e91", + 4 + ], + [ + "9dffbcd6-494f-0158-e221-12823c92a888", + 10 + ], + [ + "f2aed0ac-9afe-416f-8664-aa7ffa26d6fc", + 1 + ], + [ + "174f1f0b-b4c6-45a5-b13f-2ee8d0fb917d", + 10 + ], + [ + "35f94a83-e258-406c-a318-09f59610247c", + 41 + ], + [ + "b68fc16e-8b1b-42e2-b453-215c058844fe", + 1 + ], + [ + "b2e18506-4273-cfc2-a54e-f4bb758bba07", + 1 + ], + [ + "64f58936-fd1b-42ba-ba96-7289d5d0fa4e", + 1 + ], + [ + "697dd581-e64f-41ab-aa4a-51ecbeb7b628", + 88 + ], + [ + "d89b5e42-24bd-4d46-8412-aca8df641779", + 41 + ], + [ + "59da5d52-1232-4948-b878-597870b8e98b", + 8 + ], + [ + "26075a32-730f-4708-88e9-8c32f1599d05", + 0 + ], + [ + "6f0ed827-a609-4895-9c91-998d90180ea4", + 2 + ], + [ + "30d58be3-95ea-4282-a6e3-b159d8ebb06a", + 1 + ], + [ + "717f9ee7-e9b0-493a-88b3-91321b388107", + 16 + ], + [ + "430c4d19-7154-4970-8769-9b69df90b0e5", + 15 + ], + [ + "aafe32bd-5395-4c14-b66a-5e251032d1dd", + 1 + ], + [ + "23afe18e-4ce1-4e58-8d61-c252b953beb7", + 11 + ], + [ + "a462b7ea-f499-4e3a-99c1-ec1f8224e1b2", + 4 + ], + [ + "2eb5fdbd-01ac-4d10-8136-f38f3393a5da", + 5 + ], + [ + "509d354f-f6e6-492f-a749-85b2073c631c", + 0 + ], + [ + "b6e31b1c-d29f-11ec-857e-9f856f9970e2", + 1 + ], + [ + "4a56eb40-10f5-11dc-92d3-347eb2c96ae7", + 2 + ], + [ + "d78a4a00-e858-4697-baa8-19b5487d46b4", + 18 + ], + [ + "5579f886-933a-4c1f-83ba-087b6361b92f", + 2 + ], + [ + "612fbe52-da53-400b-910d-4f919fb1857c", + 1 + ], + [ + "a4237a36-caea-41c9-8fa2-18f858681bf3", + 5 + ], + [ + "804e3f75-7088-4b49-a4d6-8c063c7eb6dc", + 5 + ], + [ + "1ed048f4-2f2e-4c68-89d0-53a4f18f102d", + 1 + ], + [ + "fb680af2-59ef-4ba3-baa8-19b573c8443d", + 2 + ], + [ + "9950b70e-b41a-4e17-bbcc-fa0d57817fd6", + 1 + ], + [ + "ab965196-45d8-08fc-b7d7-228d78ad569e", + 1 + ] + ], + "save_game_class_name": "/Script/Pal.PalWorldOptionSaveGame" + }, + "properties": { + "Version": { + "id": null, + "value": 100, + "type": "IntProperty" + }, + "OptionWorldData": { + "struct_type": "PalOptionWorldSaveData", + "struct_id": "00000000-0000-0000-0000-000000000000", + "id": null, + "value": { + "Settings": { + "struct_type": "PalOptionWorldSettings", + "struct_id": "00000000-0000-0000-0000-000000000000", + "id": null, + "value": {}, + "type": "StructProperty" + } + }, + "type": "StructProperty" + } + }, + "trailer": "AAAAAA==" +} \ No newline at end of file diff --git a/scripts/generate-worldoption.sh b/scripts/generate-worldoption.sh new file mode 100644 index 000000000..18558c542 --- /dev/null +++ b/scripts/generate-worldoption.sh @@ -0,0 +1,208 @@ +#!/bin/bash + +# Function to map out struct type of each config attributes +config_to_struct_type() { + # Define struct type for each config attribute + case "$1" in + "DayTimeSpeedRate" | "NightTimeSpeedRate" | "ExpRate" | "PalCaptureRate" | "PalSpawnNumRate" | "PalDamageRateAttack" | \ + "PalDamageRateDefense" | "PlayerDamageRateAttack" | "PlayerDamageRateDefense" | "PlayerStomachDecreaceRate" | \ + "PlayerStaminaDecreaceRate"| "PlayerAutoHPRegeneRate" | "PlayerAutoHpRegeneRateInSleep" | "PalStomachDecreaceRate" | \ + "PalStaminaDecreaceRate" | "PalAutoHPRegeneRate" | "PalAutoHpRegeneRateInSleep" | "BuildObjectDamageRate" | \ + "BuildObjectDeteriorationDamageRate" | "CollectionDropRate" | "CollectionObjectHpRate" | "CollectionObjectRespawnSpeedRate" | \ + "EnemyDropItemRate" | "DropItemAliveMaxHours" | "GuildPlayerMaxNum" | "PalEggDefaultHatchingTime" | "WorkSpeedRate" | \ + "CoopPlayerMaxNum" | "ServerPlayerMaxNum" | "PublicPort" | "RCONPort" ) + echo "Float" # Floating point number + ;; + "DeathPenalty" ) + echo "Enum_DeathPenalty" # DeathPenalty Enums + ;; + "Difficulty" ) + echo "Enum_Difficulty" # Difficulty Enums + ;; + "DropItemMaxNum" | "DropItemMaxNum_UNKO" | "BaseCampMaxNum" | "BaseCampWorkerMaxNum" | "AutoResetGuildTimeNoOnlinePlayers" ) + echo "Int" # Integer + ;; + "bEnablePlayerToPlayerDamage" | "bEnableFriendlyFire" | "bEnableInvaderEnemy" | "bActiveUNKO" | "bEnableAimAssistPad" | \ + "bEnableAimAssistKeyboard" | "bAutoResetGuildNoOnlinePlayers" | "bIsMultiplay" | "bIsPvP" | \ + "bCanPickupOtherGuildDeathPenaltyDrop" | "bEnableNonLoginPenalty" | "bEnableFastTravel" | "bIsStartLocationSelectByMap" | \ + "bExistPlayerAfterLogout" | "bEnableDefenseOtherGuildPlayer" | "RCONEnabled" | "bUseAuth" ) + echo "Bool" # Boolean + ;; + "ServerName" | "ServerDescription" | "AdminPassword" | "ServerPassword" | "PublicIP" | "Region" | "BanListURL" ) + echo "Str" # String + ;; + *) + echo "Unknown" # Unknown type + ;; + esac +} + +# Function to convert option value based on struct type +type_cast() { + local struct=$1 + local value=$2 + + # Convert values based on struct type + if [[ $struct == "Int" ]]; then + # work around if the values are a float + echo "${value%.*}" + elif [[ $struct == "Float" ]]; then + echo "$value" + elif [[ $struct == "Bool" ]]; then + lower_value=$(echo "$value" | tr '[:upper:]' '[:lower:]') + if [[ "$lower_value" == "false" || "$value" == "0" ]]; then + echo "false" + else + echo "true" + fi + else + echo "$value" + fi +} + +# Function to convert JSON struct based on struct type +json_struct() { + local struct=$1 + local value=$2 + local struct_property="${struct}Property" + + if [[ $struct == "Unknown" ]]; then + echo "" + fi + + # Construct JSON struct based on struct type + if [[ $struct == *"Enum"* ]]; then + if [[ $struct == *"DeathPenalty"* ]]; then + echo "{\"id\": null, \"value\": {\"value\": \"EPalOptionWorldDeathPenalty::$(type_cast "$struct" "$value")\", \"type\": \"EPalOptionWorldDeathPenalty\"}, \"type\": \"EnumProperty\"}" + elif [[ $struct == *"Difficulty"* ]]; then + echo "{\"id\": null, \"value\": {\"value\": \"EPalOptionWorldDifficulty::$(type_cast "$struct" "$value")\", \"type\": \"EPalOptionWorldDifficulty\"}, \"type\": \"EnumProperty\"}" + fi + else + echo "{\"id\": null, \"value\": $(type_cast "$struct" "$value"), \"type\": \"$struct_property\"}" + fi +} + +# Function to generate JSON config +generate_json_config() { + local json_config="{}" + local config=${1//,/$'\n'} + + # Loop through each key-value pair + while IFS='=' read -r key value; do + config_properties="$(json_struct "$(config_to_struct_type "$key")" "$value")" + + if [[ -n "$config_properties" ]]; then + json_config=$(echo "$json_config" | jq --arg key "$key" --argjson config_properties "$config_properties" '.[$key] = $config_properties') + fi + done <<< "$config" + + echo "$json_config" +} + +# Function to load Palworldsettings.ini +load_palworldsettings() { + local path="$1" + local config + config=$(grep "OptionSettings" "$path") + if [[ -z "$config" ]]; then + echo "WorldOption Generator: Failed to get OptionSettings from PalWorldSettings.ini" + exit 1 + fi + + # Remove "OptionSettings=" and parentheses + config="${config#OptionSettings=}" + config="${config#\(}" + config="${config%\)}" + + echo "$config" +} + +# Python script for converting JSON to .sav +convert_json_to_sav_python=$( +cat < "$output_path/WorldOption.sav"; then + echo "WorldOption Generator: Generated WorldOption.sav file to $output_path" + echo "Generating WorldOption.sav done!" + else + echo "WorldOption Generator: WorldOption.sav generation failed." + fi +} + +############ + +echo "Generating WorldOption.sav..." + +first_time_error="Saved game not found! This is expected if it is your first time running the container. Restart the container after it initializes the save folder to generate a WorldOption file." +savegames_directory="/palworld/Pal/Saved/SaveGames/0/" + +# Check if save games directory exists +if [ ! -d "$savegames_directory" ]; then + echo "$first_time_error" + exit 1 +fi + +# Find target directory within save games directory +target_directory=$(find "$savegames_directory" -maxdepth 1 -mindepth 1 -type d -print -quit) + +# Check if target directory exists +if [ -z "$target_directory" ] || [ ! -d "$target_directory" ]; then + echo "$first_time_error" + exit 1 +fi + +# Read WorldOption JSON template +worldoption=$(cat "/home/steam/server/files/WorldOption.json.template") + +# Check if WorldOption template is found +if [ -z "$worldoption" ]; then + echo "WorldOption Generator: WorldOption.json.template not found!" + exit 1 +fi + +# Parse configuration from PalWorldSettings.ini +parsed_config=$(load_palworldsettings "/palworld/Pal/Saved/Config/LinuxServer/PalWorldSettings.ini") + +# Check if parsed configuration is empty +if [ -z "$parsed_config" ]; then + echo "WorldOption Generator: Parsed config is empty." + exit 1 +fi + +# Generate JSON settings from parsed configuration +settings_json=$(generate_json_config "$parsed_config") + +if [ "${DEBUG,,}" = true ]; then + echo "====Debug====" + echo "$settings_json" + echo "====Debug====" +fi + +# Update JSON data with generated settings +json_data=$(jq --argjson new "$settings_json" '.properties.OptionWorldData.value.Settings.value = $new' <<< "$worldoption") + +# Convert JSON data to .sav +convert_json_to_sav "$json_data" "$target_directory" \ No newline at end of file diff --git a/scripts/start.sh b/scripts/start.sh index 80ccfeebc..893d2849c 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -95,6 +95,16 @@ else /home/steam/server/compile-settings.sh fi +# WorldOption.sav generation +rm -f /palworld/Pal/Saved/SaveGames/0/*/WorldOption.sav +if [ "${GENERATE_WORLD_OPTION,,}" = true ]; then + printf "\e[0;32m%s\e[0m\n" "*****GENERATING WORLDOPTION*****" + printf "\e[0;32m%s\e[0m\n" "***Using PalWorldSettings.ini to create WorldOption.sav***" + if ! /home/steam/server/generate-worldoption.sh; then + echo "WorldOption will not be generated. PalWorldSettings.ini will apply." + fi +fi + rm -f "/home/steam/server/crontab" if [ "${BACKUP_ENABLED,,}" = true ]; then echo "BACKUP_ENABLED=${BACKUP_ENABLED,,}"