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
4 changes: 2 additions & 2 deletions README-JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ curl -fsSL https://raw.githubusercontent.com/cotore-game/mcserver-compose-kit/ma

```bash
curl -fsSL https://raw.githubusercontent.com/cotore-game/mcserver-compose-kit/main/install.sh | \
bash -s -- --version v1.1.1
bash -s -- --version v1.1.3
```

インストーラーはリリースの圧縮ファイルを取得し、SHA-256を検証して`~/.local/share/mcserver-compose-kit`へ配置します。また、`~/.local/bin`用のPATH設定を管理ブロックとして`~/.bashrc`へ追加します。
Expand Down Expand Up @@ -189,7 +189,7 @@ MOTD、難易度、ゲームモード、最大人数、オンラインモード

通常、ツールが管理する設定の正本は各サーバーの`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のファイル選択画面を開けます。既存のデータファイルは置換前にバックアップします。対応する項目を`server.env`へ取り込み、その他のキーは`CUSTOM_SERVER_PROPERTIES`へ保存します。設定の正本は引き続き`server.env`で、元ファイルも`data/server.properties`へコピーします。ワールド名やポートがこのサーバー構成と異なる場合、または未対応の書式の場合は黙って変更せずエラーにします。サーバー起動中にファイルを直接編集しないでください。
配布された`server.properties`を使う場合は、サーバーを停止して「サーバー設定 → server.propertiesをインポート」を選ぶか、`mcserver-kit server <server-id> import-properties /path/to/server.properties`を実行します。Windowsダイアログを有効にしているWSL環境では、設定画面からWindowsのファイル選択画面を開けます。既存のデータファイルは置換前にバックアップします。対応する項目を`server.env`へ取り込み、その他のキーは`CUSTOM_SERVER_PROPERTIES`へ保存します。設定の正本は引き続き`server.env`で、元ファイルも`data/server.properties`へコピーします。`\:`などJava Propertiesのエスケープを解釈します。配布ファイルの`level-name`が異なっても、ワールドの配置は`data/world`、Composeの設定は`LEVEL=world`のままです。ポートが異なる場合や書式が不正な場合は変更前にエラーにします。サーバー起動中にファイルを直接編集しないでください。

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

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Install a specific release:

```bash
curl -fsSL https://raw.githubusercontent.com/cotore-game/mcserver-compose-kit/main/install.sh | \
bash -s -- --version v1.1.1
bash -s -- --version v1.1.3
```

The installer downloads the release archive, verifies its SHA-256 checksum, and installs the program under `~/.local/share/mcserver-compose-kit`. It also adds a managed PATH block for `~/.local/bin` to `~/.bashrc`.
Expand Down Expand Up @@ -188,7 +188,7 @@ The editor covers MOTD, difficulty, game mode, player limit, online mode, whitel

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 **Server settings → Import server.properties**, or run `mcserver-kit server <server-id> import-properties /path/to/server.properties`. On WSL, the settings screen offers a Windows file picker when Windows dialogs are enabled. The existing data file is backed up before replacement. Supported properties are imported into `server.env`; additional keys are stored in `CUSTOM_SERVER_PROPERTIES`. `server.env` remains the source of truth, and the imported file is also copied to `data/server.properties`. Imports with a nonstandard world name, port, or unsupported property syntax are rejected rather than silently changing the server layout. Do not edit the file while the server is running.
For a distributed `server.properties`, stop the server and choose **Server settings → Import server.properties**, or run `mcserver-kit server <server-id> import-properties /path/to/server.properties`. On WSL, the settings screen offers a Windows file picker when Windows dialogs are enabled. The existing data file is backed up before replacement. Supported properties are imported into `server.env`; additional keys are stored in `CUSTOM_SERVER_PROPERTIES`. `server.env` remains the source of truth, and the imported file is also copied to `data/server.properties`. Java Properties escapes such as `\:` are decoded. If the distributed `level-name` differs, the toolkit keeps its existing `data/world` layout and Compose's `LEVEL=world` setting. A nonstandard server port or malformed property is rejected before changing the server. 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.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.2
1.1.3
78 changes: 64 additions & 14 deletions libexec/mcserver-kit/server-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,24 +145,73 @@ def validate_properties_source(source: Path) -> None:
raise ValueError("The selected file has no property entries")


def unescape_import_property(value: str, number: int) -> str:
"""Decode Java Properties escapes before passing values to itzg."""
result: list[str] = []
index = 0
escapes = {"t": "\t", "r": "\r", "n": "\n", "f": "\f"}
while index < len(value):
char = value[index]
if char != "\\":
result.append(char)
else:
index += 1
if index == len(value):
raise ValueError(f"Line {number}: trailing property escape")
char = value[index]
if char == "u":
digits = value[index + 1:index + 5]
if not re.fullmatch(r"[0-9a-fA-F]{4}", digits):
raise ValueError(f"Line {number}: malformed Unicode escape")
result.append(chr(int(digits, 16)))
index += 4
else:
# Java Properties removes the slash for non-special escapes too.
result.append(escapes.get(char, char))
index += 1
try:
decoded = "".join(result).encode("utf-16", "surrogatepass").decode("utf-16")
except UnicodeError as error:
raise ValueError(f"Line {number}: malformed Unicode escape") from error
if any(ord(char) < 32 for char in decoded):
raise ValueError(f"Line {number}: control characters cannot be imported")
return decoded


def parse_import_properties(source: Path) -> dict[str, str]:
"""Read the simple key=value format emitted by Minecraft without dropping entries."""
"""Parse Java Properties syntax without silently dropping server settings."""
values: dict[str, str] = {}
for number, line in enumerate(source.read_text(encoding="utf-8-sig").splitlines(), 1):
stripped = line.strip()
if not stripped or stripped.startswith(("#", "!")):
pending: str | None = None
start = 0
for number, raw in enumerate(source.read_text(encoding="utf-8-sig").splitlines(), 1):
if pending is None:
line = raw.lstrip(" \t\f")
if not line or line.startswith(("#", "!")):
continue
start = number
else:
line = pending + raw.lstrip(" \t\f")
if (len(line) - len(line.rstrip("\\"))) % 2:
pending = line[:-1]
continue
if "=" not in line:
raise ValueError(f"Line {number}: expected key=value")
key, value = line.split("=", 1)
key = key.strip()
pending = None
separator = 0
while separator < len(line):
if line[separator] == "\\":
separator += 2
elif line[separator] in "=: \t\f":
break
else:
separator += 1
key = unescape_import_property(line[:separator], start)
if not re.fullmatch(r"[A-Za-z0-9_.-]+", key):
raise ValueError(f"Line {number}: unsupported property key: {key}")
if "\\" in key or "\\" in value:
raise ValueError(f"Line {number}: escaped properties are not supported")
values[key] = value
if values.get("level-name", "world") != "world":
raise ValueError("level-name must be world for this server layout")
raise ValueError(f"Line {start}: unsupported property key: {key}")
remainder = line[separator:].lstrip(" \t\f")
if remainder.startswith(("=", ":")):
remainder = remainder[1:]
values[key] = unescape_import_property(remainder.lstrip(" \t\f"), start)
if pending is not None:
raise ValueError(f"Line {start}: unfinished property continuation")
if values.get("server-port", "25565") != "25565":
raise ValueError("server-port must be 25565 for this server layout")
return values
Expand All @@ -176,6 +225,7 @@ def import_properties(server_dir: Path, source: Path) -> Path | None:
for env_key, property_key in PROPERTY_KEYS.items():
if property_key in imported:
values[env_key] = imported[property_key]
# create-server.sh stores the selected save at data/world; Compose sets LEVEL=world.
known = set(PROPERTY_KEYS.values()) | {"level-name", "server-port"}
extras = [f"{key}={value}" for key, value in imported.items() if key not in known]
values["CUSTOM_SERVER_PROPERTIES"] = "\n".join(extras)
Expand Down
39 changes: 28 additions & 11 deletions tests/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -439,13 +439,17 @@ test_local_installation() {

test_home_dashboard() {
local temp_dir="$1"
local repo_version next_version major minor patch
local config_file="${temp_dir}/home/config.yml"
local fake_bin="${temp_dir}/home/bin"
local whiptail_log="${temp_dir}/home/whiptail.log"
local update_cache="${temp_dir}/home/update-cache"
local curl_count="${temp_dir}/home/curl-count"
local output

repo_version="$(head -n 1 "${REPO_ROOT}/VERSION")"
IFS=. read -r major minor patch <<<"$repo_version"
next_version="v${major}.${minor}.$((patch + 1))"

mkdir -p "$fake_bin"
cat >"$config_file" <<CONFIG
paths:
Expand All @@ -456,24 +460,23 @@ CONFIG
printf '%s\n' "$*" >>"$MCSERVER_KIT_TEST_WHIPTAIL_LOG"
printf 'exit' >&2
WHIPTAIL
cat >"${fake_bin}/curl" <<'CURL'
#!/usr/bin/env bash
printf x >>"$MCSERVER_KIT_TEST_CURL_COUNT"
printf '%s' 'https://github.com/cotore-game/mcserver-compose-kit/releases/tag/v1.1.1'
CURL
chmod +x "${fake_bin}/whiptail" "${fake_bin}/curl"
chmod +x "${fake_bin}/whiptail"
mkdir -p "$update_cache"
printf '%s\n%s\n' "$(date +%s)" "$next_version" >"${update_cache}/update-check"
output="$(MCSERVER_KIT_CURRENT_VERSION="$repo_version" MCSERVER_KIT_UPDATE_CACHE_DIR="$update_cache" \
bash "${REPO_ROOT}/libexec/mcserver-kit/update.sh" --cached-quiet)"
assert_equal "$next_version" "$output" 'the dashboard fixture contains a newer cached release'

output="$(MCSERVER_KIT_LANG=en bash "${REPO_ROOT}/mcserver-kit")"
assert_equal 'present' "$(grep -q 'mcserver-kit home' <<<"$output" && printf present)" 'non-interactive no-argument use shows help instead of starting creation'

PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \
MCSERVER_KIT_CURRENT_VERSION=1.1.0 MCSERVER_KIT_UPDATE_CACHE_DIR="$update_cache" \
MCSERVER_KIT_TEST_CURL_COUNT="$curl_count" \
MCSERVER_KIT_CURRENT_VERSION="$repo_version" MCSERVER_KIT_UPDATE_CACHE_DIR="$update_cache" \
MCSERVER_KIT_TUI_TEST=true MCSERVER_KIT_TEST_WHIPTAIL_LOG="$whiptail_log" \
bash "${REPO_ROOT}/mcserver-kit" home
assert_equal 'present' "$(grep -q -- '--backtitle mcserver-kit' "$whiptail_log" && printf present)" 'the home command opens the interactive dashboard'
assert_equal 'present' "$(grep -q 'servers Servers' "$whiptail_log" && printf present)" 'the dashboard exposes server management'
assert_equal 'present' "$(grep -q 'Update available: v1.1.1' "$whiptail_log" && printf present)" 'the dashboard announces a newer cached release'
assert_equal 'present' "$(grep -Fq "Update available: $next_version" "$whiptail_log" && printf present)" 'the dashboard announces a newer cached release'
}

test_home_screen_transition_after_current_update() {
Expand Down Expand Up @@ -754,8 +757,13 @@ PROPERTIES
# distributed settings
motd=Distributed MOTD
difficulty=hard
level-name=Fantasia_Arena_World
level-type=minecraft\:normal
custom-setting=preserve-me
plugin.option=enabled
plugin.label=\u00a7a
plugin.description=first \
second
PROPERTIES
cat >"${fake_bin}/docker" <<'DOCKER'
#!/usr/bin/env bash
Expand Down Expand Up @@ -795,6 +803,14 @@ PROPERTIES
bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/incompatible/server.properties"
assert_equal 'Old MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'rejected import leaves the environment unchanged'

cat >"${source_dir}/incompatible/server.properties" <<'PROPERTIES'
plugin.option=\u12G4
PROPERTIES
assert_fails 'malformed Unicode escapes are rejected before migration' \
env PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \
bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/incompatible/server.properties"
assert_equal 'Old MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'malformed escape leaves the environment unchanged'

assert_fails 'import refuses to replace properties while the server is running' \
env PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \
MCSERVER_KIT_TEST_RUNNING=true bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/server.properties"
Expand All @@ -806,7 +822,8 @@ PROPERTIES
assert_equal 'true' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'import keeps server.env authoritative'
assert_equal 'Distributed MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'import converts managed properties to environment settings'
assert_equal 'hard' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" DIFFICULTY)" 'import updates the difficulty'
assert_equal $'custom-setting=preserve-me\nplugin.option=enabled' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" CUSTOM_SERVER_PROPERTIES)" 'import converts extra properties to CUSTOM_SERVER_PROPERTIES'
assert_equal $'level-type=minecraft:normal\ncustom-setting=preserve-me\nplugin.option=enabled\nplugin.label=§a\nplugin.description=first second' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" CUSTOM_SERVER_PROPERTIES)" 'import decodes Java escapes and keeps extra properties'
assert_equal 'absent' "$(! python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" CUSTOM_SERVER_PROPERTIES | grep -q '^level-name=' && printf absent)" 'source world name does not override the managed world directory'
assert_equal 'present' "$(grep -q 'custom-setting=preserve-me' "${server_dir}/data/server.properties" && printf present)" 'import keeps custom settings'
assert_equal 'present' "$(grep -q 'motd=Old MOTD' "${server_dir}"/data/server.properties.mcserver-kit.*.bak && printf present)" 'import backs up the previous properties'
assert_equal 'present' "$(grep -q 'Previous server.properties backup' <<<"$output" && printf present)" 'import reports the backup location'
Expand Down