Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README-JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ mcserver-kit server <server-id> logs
mcserver-kit server <server-id> logs --no-follow
mcserver-kit server <server-id> down
mcserver-kit server <server-id> properties
mcserver-kit server <server-id> import-properties /path/to/server.properties
mcserver-kit server <server-id> open data
mcserver-kit server <server-id> open server
```

`stop`と`shutdown`はコンテナを削除せず停止します。`down`はコンテナとネットワークを削除します。いずれもサーバーの`data/`は削除しません。
Expand All @@ -184,7 +187,11 @@ mcserver-kit server <server-id> properties

MOTD、難易度、ゲームモード、最大人数、オンラインモード、ホワイトリスト、OP、飛行、コマンドブロック、PvP、描画・シミュレーション距離、スポーン保護、ネザー、Mob/NPC生成、リソースパックなどを編集できます。

ツールが管理する設定の正本は各サーバーの`server.env`です。Docker Composeが値を`itzg/minecraft-server`へ渡し、コンテナ起動時に`server.properties`へ反映します。
通常、ツールが管理する設定の正本は各サーバーの`server.env`です。Docker Composeが値を`itzg/minecraft-server`へ渡し、コンテナ起動時に`server.properties`へ反映します。

配布された`server.properties`を使う場合は、サーバーを停止してホーム画面の「server.propertiesをインポート」を選ぶか、`mcserver-kit server <server-id> import-properties /path/to/server.properties`を実行します。Windowsダイアログを有効にしているWSL環境では、ホーム画面からWindowsのファイル選択画面を開けます。既存ファイルは置換前にバックアップされます。インポートしたサーバーだけ、プロパティ設定の正本が`data/server.properties`に切り替わり、設定画面もそのファイルを読み書きします。ホワイトリストとOPのメンバー一覧は引き続き`server.env`で管理します。起動中にファイルを直接編集しないでください。

ホーム画面からサーバーフォルダ、または永続データの`data/`をWindowsのExplorerで開けます。コマンドでは`mcserver-kit server <server-id> open server`または`open data`です。WSLとExplorerの連携が必要です。

古い形式のサーバーを初めて開く場合は、移行前に確認画面を表示します。元のComposeは`compose.yaml.mcserver-kit.bak`として保存します。

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ mcserver-kit server <server-id> logs
mcserver-kit server <server-id> logs --no-follow
mcserver-kit server <server-id> down
mcserver-kit server <server-id> properties
mcserver-kit server <server-id> import-properties /path/to/server.properties
mcserver-kit server <server-id> open data
mcserver-kit server <server-id> open server
```

`stop` and `shutdown` stop the container without removing it. `down` removes the container and network. These commands do not delete the server's `data/` directory.
Expand All @@ -183,7 +186,11 @@ mcserver-kit server <server-id> properties

The editor covers MOTD, difficulty, game mode, player limit, online mode, whitelist, operators, flight, command blocks, PvP, view and simulation distance, spawn protection, Nether and entity spawning, and resource packs.

`server.env` is the source of truth for settings managed by the toolkit. Docker Compose passes these values to `itzg/minecraft-server`, which applies them to `server.properties` when the container starts.
Normally, `server.env` is the source of truth for settings managed by the toolkit. Docker Compose passes these values to `itzg/minecraft-server`, which applies them to `server.properties` when the container starts.

For a distributed `server.properties`, stop the server and choose **Import server.properties** in the dashboard, or run `mcserver-kit server <server-id> import-properties /path/to/server.properties`. On WSL, the dashboard offers a Windows file picker when Windows dialogs are enabled. The existing file is backed up before replacement. Only that server switches to using `data/server.properties` as the source of truth for property settings; the editor then reads and writes that file. Whitelist and OP member lists remain in `server.env`. Do not edit the file while the server is running.

The dashboard can also open the server folder or its persistent `data/` folder in Windows Explorer. From a terminal, use `mcserver-kit server <server-id> open server` or `open data`. This requires WSL interop with Explorer.

When an older server is opened for the first time, the editor asks before migrating it. The original Compose file is saved as `compose.yaml.mcserver-kit.bak`.

Expand Down
24 changes: 22 additions & 2 deletions libexec/mcserver-kit/home-tui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,18 @@ run_and_show() {
}

server_action_menu() {
local id="$1" directory="$2" choice
local id="$1" directory="$2" choice output
while true; do
choice="$(whiptail --title "$id" --menu "$(tr home.server_status "$(server_status "$directory")")" 21 78 11 \
choice="$(whiptail --title "$id" --menu "$(tr home.server_status "$(server_status "$directory")")" 23 78 13 \
start "$(tr home.start)" \
stop "$(tr home.stop)" \
restart "$(tr home.restart)" \
status "$(tr home.status)" \
logs "$(tr home.logs)" \
properties "$(tr home.properties)" \
import-properties "$(tr home.import_properties)" \
open-data "$(tr home.open_data)" \
open-server "$(tr home.open_server)" \
down "$(tr home.down)" \
back "$(tr tui.back)" \
3>&1 1>&2 2>&3)" || return
Expand All @@ -155,6 +158,23 @@ server_action_menu() {
properties)
"${SCRIPT_DIR}/server-manager.sh" server "$id" properties || true
;;
import-properties)
if whiptail --yesno "$(tr home.import_confirm "$id")" 12 76; then
clear
"${SCRIPT_DIR}/server-manager.sh" server "$id" import-properties || true
pause_for_enter
fi
;;
open-data | open-server)
new_temp_file output
if [[ "$choice" == open-data ]]; then
"${SCRIPT_DIR}/server-manager.sh" server "$id" open data >"$output" 2>&1 ||
whiptail --title "$(tr common.error)" --textbox "$output" 14 78
else
"${SCRIPT_DIR}/server-manager.sh" server "$id" open server >"$output" 2>&1 ||
whiptail --title "$(tr common.error)" --textbox "$output" 14 78
fi
;;
down)
if whiptail --yesno "$(tr home.down_confirm "$id")" 10 72; then
run_and_show "$id" "${SCRIPT_DIR}/server-manager.sh" server "$id" down
Expand Down
99 changes: 97 additions & 2 deletions libexec/mcserver-kit/server-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import shutil
import sys
import tempfile
from datetime import datetime
from pathlib import Path

MANAGED_DEFAULTS = {
Expand Down Expand Up @@ -116,6 +117,68 @@ def read_properties(path: Path) -> dict[str, str]:
return values


def set_property(path: Path, env_key: str, value: str) -> None:
property_key = PROPERTY_KEYS[env_key]
if "\n" in value or "\r" in value:
raise ValueError("Property values cannot contain newlines")
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
replacement = f"{property_key}={value}\n"
indices = [
index for index, line in enumerate(lines)
if line.partition("=")[0].strip() == property_key
]
if indices:
lines[indices[-1]] = replacement
else:
if lines and not lines[-1].endswith("\n"):
lines[-1] += "\n"
lines.append(replacement)
atomic_write(path, "".join(lines))


def validate_properties_source(source: Path) -> None:
if not source.is_file():
raise ValueError(f"Not a file: {source}")
if source.name != "server.properties":
raise ValueError("Select a file named server.properties")
if not read_properties(source):
raise ValueError("The selected file has no property entries")


def import_properties(server_dir: Path, source: Path) -> Path | None:
validate_properties_source(source)

destination = server_dir / "data" / "server.properties"
destination.parent.mkdir(parents=True, exist_ok=True)
if source.resolve() == destination.resolve():
raise ValueError("The source is already this server's server.properties")
backup = None
if destination.exists():
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
backup = destination.with_name(f"server.properties.mcserver-kit.{stamp}.bak")
suffix = 1
while backup.exists():
backup = destination.with_name(f"server.properties.mcserver-kit.{stamp}.{suffix}.bak")
suffix += 1
shutil.copy2(destination, backup)

descriptor, temporary_name = tempfile.mkstemp(prefix="server.properties.", dir=destination.parent)
try:
with os.fdopen(descriptor, "wb") as output, source.open("rb") as input_file:
shutil.copyfileobj(input_file, output)
os.chmod(temporary_name, 0o644)
env_path = server_dir / "server.env"
values = read_env(env_path)
values["OVERRIDE_SERVER_PROPERTIES"] = "false"
write_env(env_path, values)
os.replace(temporary_name, destination)
finally:
if os.path.exists(temporary_name):
os.unlink(temporary_name)

return backup


def compose_environment(path: Path, dotenv: dict[str, str]) -> dict[str, str]:
values: dict[str, str] = {}
if not path.is_file():
Expand Down Expand Up @@ -195,7 +258,7 @@ def migrate(server_dir: Path) -> None:
backup = server_dir / "compose.yaml.mcserver-kit.bak"
if not target.exists() and not backup.exists():
shutil.copy2(compose, backup)
values: dict[str, str] = {}
values: dict[str, str] = dict(existing)
for env_key, default in MANAGED_DEFAULTS.items():
property_key = PROPERTY_KEYS.get(env_key, "")
values[env_key] = existing.get(
Expand All @@ -214,13 +277,45 @@ def migrate(server_dir: Path) -> None:

def main() -> int:
if len(sys.argv) < 3:
print("usage: server-config.py get|set|migrate TARGET [KEY] [VALUE]", file=sys.stderr)
print("usage: server-config.py get|set|migrate|property-get|property-set|import-properties TARGET [KEY] [VALUE]", file=sys.stderr)
return 2
operation = sys.argv[1]
target = Path(sys.argv[2])
if operation == "migrate" and len(sys.argv) == 3:
migrate(target)
return 0
if operation == "validate-properties" and len(sys.argv) == 3:
try:
validate_properties_source(target)
except (OSError, ValueError, UnicodeError) as error:
print(f"Invalid server.properties: {error}", file=sys.stderr)
return 1
return 0
if operation == "import-properties" and len(sys.argv) == 4:
try:
backup = import_properties(target, Path(sys.argv[3]))
except (OSError, ValueError, UnicodeError) as error:
print(f"Import failed: {error}", file=sys.stderr)
return 1
if backup:
print(backup)
return 0
if operation == "property-get" and len(sys.argv) in (4, 5):
key = PROPERTY_KEYS.get(sys.argv[3])
if key is None:
return 2
default = sys.argv[4] if len(sys.argv) == 5 else ""
print(read_properties(target).get(key, default))
return 0
if operation == "property-set" and len(sys.argv) == 5:
if sys.argv[3] not in PROPERTY_KEYS:
return 2
try:
set_property(target, sys.argv[3], sys.argv[4])
except (OSError, ValueError, UnicodeError) as error:
print(f"Could not edit server.properties: {error}", file=sys.stderr)
return 1
return 0
if operation == "get" and len(sys.argv) in (4, 5):
values = read_env(target)
default = sys.argv[4] if len(sys.argv) == 5 else ""
Expand Down
65 changes: 65 additions & 0 deletions libexec/mcserver-kit/server-manager.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${MCSERVER_KIT_CONFIG:-${HOME}/.config/mcserver-compose-kit/config.yml}"
CONFIG_VALUE="${SCRIPT_DIR}/config-value.py"
CONFIG_TOOL="${SCRIPT_DIR}/server-config.py"
WINDOWS_DIALOG="${SCRIPT_DIR}/windows-dialog.ps1"

# shellcheck source=libexec/mcserver-kit/i18n.sh
source "${SCRIPT_DIR}/i18n.sh"
Expand Down Expand Up @@ -82,6 +84,61 @@ compose_in() {
)
}

require_stopped() {
local directory="$1" running
running="$(compose_in "$directory" ps --status running --services)" || die "$(tr server.status_failed)"
[[ -z "$running" ]] || die "$(tr server.stop_before_import)"
}

choose_properties_file() {
local enabled dialog_path encoded selected
enabled="$(python3 "$CONFIG_VALUE" get "$CONFIG_FILE" ui windows_dialogs 2>/dev/null || printf true)"
if [[ "$enabled" == true ]] && command -v wslpath >/dev/null 2>&1 &&
[[ -x /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe && -f "$WINDOWS_DIALOG" ]]; then
dialog_path="$(wslpath -w "$WINDOWS_DIALOG")"
encoded="$(/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe \
-NoProfile -ExecutionPolicy Bypass -File "$dialog_path" -Mode SelectProperties 2>/dev/null)" || return 1
encoded="${encoded//$'\r'/}"
selected="$(printf '%s' "$encoded" | base64 --decode)" || return 1
wslpath -u "$selected"
return
fi
[[ -t 0 ]] || die "$(tr server.import_usage)"
read -r -p "$(tr server.import_path_prompt)" selected
[[ -n "$selected" ]] || return 1
printf '%s' "$selected"
}

import_properties() {
local directory="$1" source="${2-}" backup
require_stopped "$directory"
if [[ -z "$source" ]]; then
source="$(choose_properties_file)" || return 0
fi
[[ -f "$source" ]] || die "$(tr server.import_source_missing "$source")"
python3 "$CONFIG_TOOL" validate-properties "$source" || die "$(tr server.import_failed)"
python3 "$CONFIG_TOOL" migrate "$directory" || die "$(tr server.import_failed)"
backup="$(python3 "$CONFIG_TOOL" import-properties "$directory" "$source")" || die "$(tr server.import_failed)"
compose_in "$directory" config --quiet || die "$(tr properties.compose_invalid)"
[[ -z "$backup" ]] || printf '%s\n' "$(tr server.import_backup "$backup")"
printf '%s\n' "$(tr server.import_done "${directory}/data/server.properties")"
}

open_folder() {
local directory="$1" part="${2:-data}" target windows_path
case "$part" in
data) target="${directory}/data" ;;
server) target="$directory" ;;
*) die "$(tr server.open_usage)" ;;
esac
[[ -d "$target" ]] || die "$(tr server.open_missing "$target")"
if ! command -v wslpath >/dev/null 2>&1 || ! command -v explorer.exe >/dev/null 2>&1; then
die "$(tr server.explorer_unavailable)"
fi
windows_path="$(wslpath -w "$target")" || die "$(tr server.explorer_unavailable)"
explorer.exe "$windows_path"
}

manage_server() {
local id="${1-}"
local action="${2-}"
Expand Down Expand Up @@ -124,6 +181,14 @@ manage_server() {
properties)
exec "${SCRIPT_DIR}/server-properties-tui.sh" "$id" "$directory"
;;
import-properties)
[[ $# -le 1 ]] || die "$(tr server.import_usage)"
import_properties "$directory" "${1-}"
;;
open)
[[ $# -le 1 ]] || die "$(tr server.open_usage)"
open_folder "$directory" "${1:-data}"
;;
*)
die "$(tr server.unknown_action "$action")"
;;
Expand Down
31 changes: 29 additions & 2 deletions libexec/mcserver-kit/server-properties-tui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
SERVER_ID="${1-}"
SERVER_DIR="${2-}"
SERVER_ENV="${SERVER_DIR}/server.env"
SERVER_PROPERTIES="${SERVER_DIR}/data/server.properties"
CONFIG_TOOL="${SCRIPT_DIR}/server-config.py"
changed=false
manual_properties=false

# shellcheck source=libexec/mcserver-kit/i18n.sh
source "${SCRIPT_DIR}/i18n.sh"
Expand Down Expand Up @@ -40,11 +42,27 @@ die() {
}

setting_get() {
python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2"
if [[ "$manual_properties" == true ]]; then
case "$1" in
WHITELIST | EXISTING_WHITELIST_FILE | OPS | EXISTING_OPS_FILE)
python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2" ;;
*) python3 "$CONFIG_TOOL" property-get "$SERVER_PROPERTIES" "$1" "$2" ;;
esac
else
python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2"
fi
}

setting_set() {
python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2"
if [[ "$manual_properties" == true ]]; then
case "$1" in
WHITELIST | EXISTING_WHITELIST_FILE | OPS | EXISTING_OPS_FILE)
python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2" ;;
*) python3 "$CONFIG_TOOL" property-set "$SERVER_PROPERTIES" "$1" "$2" ;;
esac
else
python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2"
fi
changed=true
}

Expand Down Expand Up @@ -212,6 +230,15 @@ main() {
whiptail --yesno "$(tr properties.migration_prompt)" 11 76 || return
fi
python3 "$CONFIG_TOOL" migrate "$SERVER_DIR"
if [[ "$(python3 "$CONFIG_TOOL" get "$SERVER_ENV" OVERRIDE_SERVER_PROPERTIES true)" == false ]]; then
[[ -f "$SERVER_PROPERTIES" ]] || die "$(tr properties.file_missing)"
local running
running="$(cd "$SERVER_DIR" && docker compose ps --status running --services)" || die "$(tr server.status_failed)"
if [[ -n "$running" ]]; then
die "$(tr properties.stop_first "$SERVER_ID")"
fi
manual_properties=true
fi
(cd "$SERVER_DIR" && docker compose config --quiet) || die "$(tr properties.compose_invalid)"

while true; do
Expand Down
14 changes: 13 additions & 1 deletion libexec/mcserver-kit/windows-dialog.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
param(
[Parameter(Mandatory = $true)]
[ValidateSet('SelectWorld', 'InputMotd', 'Check')]
[ValidateSet('SelectWorld', 'SelectProperties', 'InputMotd', 'Check')]
[string]$Mode,

[string]$DefaultValue = ''
Expand Down Expand Up @@ -58,6 +58,18 @@ if ($Mode -eq 'SelectWorld') {
exit 0
}

if ($Mode -eq 'SelectProperties') {
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.Title = 'Select server.properties / server.propertiesを選択'
$dialog.Filter = 'server.properties|server.properties|All files (*.*)|*.*'
$dialog.CheckFileExists = $true
if ($dialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) {
exit 1
}
Write-Utf8Base64 $dialog.FileName
exit 0
}

if ($Mode -eq 'InputMotd') {
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Minecraft Server Kit'
Expand Down
Loading