From cd9e5b259d7c8c2b5b2f0ea37c9343eea3338806 Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 30 Sep 2026 16:36:21 +0900 Subject: [PATCH] android: build.sh installs and syncs the game files when a phone is attached After building the APK, android/build.sh now picks the attached device (ANDROID_SERIAL for several), installs the APK (reinstalling on a signature mismatch), syncs the zh client into files/Client and, with --launch, wakes the phone and starts the app. Without a device it only builds. push-client.sh is now incremental (android/sync_client.py): the device keeps files/Client/.mt_sync (size, mtime, path of what the last sync wrote), so later syncs send only new and changed files as one tar streamed through `adb shell -T run-as` (no copy in /data/local/tmp) and delete the ones gone locally. Only game resources are synced; settings and logs the app writes there are kept. --locale zh rebuilds build/zh-locale/Client first when its inputs changed. android/adb_env.sh picks adb and falls back to server port 5038 when :5037 cannot bind. Verified against a fake adb device: first sync (1.4 GB, byte-identical), no-op resync, changed files, deletions, locale switch, --full, the install/launch path and the no-device path. Co-Authored-By: Claude Opus 5.5 --- .../references/project-map.md | 2 +- README.md | 7 +- android/adb_env.sh | 41 ++++ android/build.sh | 70 +++++- android/push-client.sh | 45 +--- android/sync_client.py | 223 ++++++++++++++++++ docs/ANDROID-TESTING.md | 18 +- docs/PORT-PLAN.md | 4 +- docs/ZH-LOCALE.md | 2 +- src/host/README.md | 15 +- 10 files changed, 363 insertions(+), 64 deletions(-) create mode 100644 android/adb_env.sh create mode 100644 android/sync_client.py diff --git a/.agents/skills/metin2-40250-parity-audit/references/project-map.md b/.agents/skills/metin2-40250-parity-audit/references/project-map.md index 5420a837..80e229cb 100644 --- a/.agents/skills/metin2-40250-parity-audit/references/project-map.md +++ b/.agents/skills/metin2-40250-parity-audit/references/project-map.md @@ -76,7 +76,7 @@ The 106 `project/*_system.gd` files that only their own `test_*_parity.gd` loade cmake -S . -B build/native -DCMAKE_BUILD_TYPE=Release -DMT_BUILD_NATIVE_RENDER=ON -DCMAKE_PREFIX_PATH=/opt/homebrew cmake --build build/native -j8 && ctest --test-dir build/native --output-on-failure -j4 ./scripts/port_gate.sh macos android # build/port-gate -./android/build.sh Release --install # build/android + APK +./android/build.sh Release # build/android + APK; installs + syncs zh assets when a phone is attached ``` All build trees live under `build/` (gitignored). Always finish with `git diff --check`. diff --git a/README.md b/README.md index a02e57db..d62df8f6 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,11 @@ cmake --build build/release --target mt_native_render -j8 ## Android ```sh -./android/build.sh Release --install -./android/push-client.sh /path/to/40250/Client -adb shell am start -n org.metin2port.client/.MainActivity +./android/build.sh # 构建 APK;连着手机时自动安装并增量推送中文资源 +./android/build.sh --launch # 同上,完成后唤醒手机并启动 ``` -APK 由 SDLActivity、`libSDL3.so` 和 Vulkan 原生 `libmain.so` 组成。40250 `Client` 资源单独复制到应用数据目录;`--live-server HOST:AUTH_PORT:GAME_PORT --login-screen` 可连接服务器。构建与启动细节见 [src/host/README.md](src/host/README.md)。 +APK 由 SDLActivity、`libSDL3.so` 和 Vulkan 原生 `libmain.so` 组成。40250 `Client` 资源单独同步到应用数据目录(`android/push-client.sh`,只传有变化的文件);`--live-server HOST:AUTH_PORT:GAME_PORT --login-screen` 可连接服务器。构建与启动细节见 [src/host/README.md](src/host/README.md)。 ## 测试 diff --git a/android/adb_env.sh b/android/adb_env.sh new file mode 100644 index 00000000..539bca84 --- /dev/null +++ b/android/adb_env.sh @@ -0,0 +1,41 @@ +# Sourced by the android/ scripts: picks adb and the target device. +# ADB adb binary (default: $ANDROID_HOME/platform-tools/adb, else adb on PATH) +# ANDROID_SERIAL device to use when several are attached (adb honours it itself) +# An adb server that cannot bind :5037 ("Address already in use" with nothing listening, seen on this +# Mac after sleep) is retried on :5038 through ANDROID_ADB_SERVER_PORT. +SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}}" +PKG=org.metin2port.client +if [ -z "${ADB:-}" ]; then + if [ -x "$SDK/platform-tools/adb" ]; then ADB="$SDK/platform-tools/adb"; else ADB="$(command -v adb || true)"; fi +fi +[ -n "$ADB" ] || { echo "adb not found (install Android platform-tools or set ADB)" >&2; return 1 2>/dev/null || exit 1; } +if ! "$ADB" start-server >/dev/null 2>&1; then + export ANDROID_ADB_SERVER_PORT=5038 + "$ADB" start-server >/dev/null 2>&1 || { echo "adb server does not start on :5037 or :5038" >&2; return 1 2>/dev/null || exit 1; } + echo "adb: :5037 unusable, using :5038" +fi +export ADB + +# adb_device: sets DEVICE to the one ready device (or ANDROID_SERIAL) and returns 0; returns 1 with a +# message when none is ready (unauthorized / offline devices are named so the user can fix them). +adb_device() { + local ready others + ready=$("$ADB" devices | awk 'NR > 1 && $2 == "device" { print $1 }') + others=$("$ADB" devices | awk 'NR > 1 && NF >= 2 && $2 != "device" { print $1 " (" $2 ")" }') + if [ -n "${ANDROID_SERIAL:-}" ]; then + if printf '%s\n' "$ready" | grep -qxF "$ANDROID_SERIAL"; then DEVICE="$ANDROID_SERIAL"; return 0; fi + echo "device $ANDROID_SERIAL is not ready" >&2 + return 1 + fi + case $(printf '%s' "$ready" | grep -c .) in + 1) DEVICE="$ready"; export ANDROID_SERIAL="$DEVICE"; return 0 ;; + 0) + if [ -n "$others" ]; then + echo "device attached but not ready: $others (unlock the phone and allow USB debugging)" >&2 + else + echo "no Android device attached" >&2 + fi + return 1 ;; + *) echo "several devices attached, pick one with ANDROID_SERIAL:" $ready >&2; return 1 ;; + esac +} diff --git a/android/build.sh b/android/build.sh index 74616713..40ef46e4 100755 --- a/android/build.sh +++ b/android/build.sh @@ -1,20 +1,35 @@ #!/usr/bin/env bash -# Build the native (SDL3 + Vulkan) 40250 client as an Android arm64 APK. -# ./android/build.sh [Debug|Release] [--install] +# Build the native (SDL3 + Vulkan) 40250 client as an Android arm64 APK and, when a phone is attached, +# install it and bring its game files up to date, so the app is ready to play. +# ./android/build.sh [Debug|Release] [--no-deploy] [--no-push] [--locale XX] [--full-push] [--launch] # Output: android/app/build/outputs/apk/debug/app-debug.apk -# The 1.4 GB Client/ directory is not in the APK; push it once with ./android/push-client.sh. +# Deploy (skipped when no device is ready; ANDROID_SERIAL picks one of several): +# adb install -r, then ./android/push-client.sh --locale XX (default zh), which sends only the game +# files changed since the last push (the first push is the whole 1.4 GB client). --no-push installs +# only, --full-push resends everything, --launch wakes the phone and starts the app afterwards. set -euo pipefail cd "$(dirname "$0")/.." REPO="$PWD" CONFIG=Release -INSTALL=0 -for a in "$@"; do - case "$a" in - Debug|Release) CONFIG="$a" ;; - --install) INSTALL=1 ;; - *) echo "usage: $0 [Debug|Release] [--install]" >&2; exit 2 ;; +DEPLOY=1 +PUSH=1 +LOCALE=zh +FULL_PUSH=0 +LAUNCH=0 +usage="usage: $0 [Debug|Release] [--no-deploy] [--no-push] [--locale XX] [--full-push] [--launch]" +while [ $# -gt 0 ]; do + case "$1" in + Debug|Release) CONFIG="$1" ;; + --install) ;; # the default now; kept for old command lines + --no-deploy) DEPLOY=0 ;; + --no-push) PUSH=0 ;; + --locale) LOCALE="${2:?--locale needs a locale code}"; shift ;; + --full-push) FULL_PUSH=1 ;; + --launch) LAUNCH=1 ;; + *) echo "$usage" >&2; exit 2 ;; esac + shift done API=24 OUT="$REPO/build/android" @@ -84,6 +99,39 @@ variant=Debug # debug-signed either way (sideload); CONFIG picks the native opt APK="$REPO/android/app/build/outputs/apk/debug/app-debug.apk" ls -la "$APK" -if [ "$INSTALL" = 1 ]; then - adb install -r "$APK" +[ "$DEPLOY" = 1 ] || exit 0 +. android/adb_env.sh +if ! adb_device; then + echo "APK built; install and push skipped. Attach the phone and rerun (or ./android/push-client.sh)." + exit 0 +fi +echo "device: $DEVICE ($("$ADB" shell getprop ro.product.model | tr -d '\r'))" +if ! out=$("$ADB" install -r "$APK" 2>&1); then + echo "$out" >&2 + case "$out" in + *INSTALL_FAILED_UPDATE_INCOMPATIBLE*|*INSTALL_FAILED_VERSION_DOWNGRADE*) + # Signed by another machine's debug key (or a newer build): only a reinstall helps. That drops the + # app's data, so the push below starts over with the whole client. + echo "reinstalling $PKG (the installed copy has another signature; its data is removed)" + "$ADB" uninstall "$PKG" >/dev/null + "$ADB" install "$APK" ;; + *) + echo "install failed; on some phones USB installs must be allowed in developer options and confirmed on screen" >&2 + exit 1 ;; + esac +fi +echo "installed $PKG" +if [ "$PUSH" = 1 ]; then + push_args=(--locale "$LOCALE") + [ "$FULL_PUSH" = 1 ] && push_args+=(--full) + python3 android/sync_client.py "${push_args[@]}" +fi +if [ "$LAUNCH" = 1 ]; then + # The app exits with a Vulkan surface error when started behind the lock screen. + "$ADB" shell input keyevent KEYCODE_WAKEUP + if "$ADB" shell dumpsys window | grep -qE 'mDreamingLockscreen=true|isKeyguardShowing=true|mKeyguardShowing=true'; then + echo "the phone is locked: unlock it, then start Metin2 from the launcher" >&2 + else + "$ADB" shell am start -n "$PKG/.MainActivity" >/dev/null && echo "started $PKG" + fi fi diff --git a/android/push-client.sh b/android/push-client.sh index 6ad49272..1c33c9e6 100755 --- a/android/push-client.sh +++ b/android/push-client.sh @@ -1,39 +1,14 @@ #!/usr/bin/env bash -# Copy the 40250 Client/ directory into the Android app's private files/ (SDL_GetPrefPath), where -# src/host looks for files/Client/pack/Index. Needs the debug APK installed (run-as). -# ./android/push-client.sh [--locale XX] [CLIENT_DIR] -# --locale XX writes the pushed locale.cfg from CLIENT_DIR/locale_XX.cfg. --locale zh defaults -# CLIENT_DIR to build/zh-locale/Client (scripts/build_zh_locale.py), whose pack/ registers locale_zh. +# Sync the 40250 game files into the Android app's private files/ (SDL_GetPrefPath), where src/host looks +# for files/Client/pack/Index. Only files changed since the last sync are sent (android/sync_client.py). +# Needs the debug APK installed (run-as). +# ./android/push-client.sh [--locale XX] [--full] [--dry-run] [CLIENT_DIR] +# --locale XX writes the pushed locale.cfg from CLIENT_DIR/locale_XX.cfg. --locale zh defaults CLIENT_DIR +# to build/zh-locale/Client (scripts/build_zh_locale.py, rebuilt first when its inputs changed). +# --full deletes the synced resources on the device and pushes everything again. # Symbolic links in CLIENT_DIR (the zh client links the original packs) are copied as their targets. set -euo pipefail cd "$(dirname "$0")/.." -PKG=org.metin2port.client -LOCALE="" -if [ "${1:-}" = "--locale" ]; then - LOCALE="${2:?--locale needs a locale code}" - shift 2 -fi -if [ "$LOCALE" = zh ]; then - CLIENT="${1:-build/zh-locale/Client}" -else - CLIENT="${1:-Client}" -fi -[ -f "$CLIENT/pack/Index" ] || { echo "$CLIENT/pack/Index not found" >&2; exit 1; } -# The archive's top directory is basename(CLIENT DIR); the app reads files/Client. -[ "$(basename "$CLIENT")" = Client ] || { echo "CLIENT_DIR must be named Client" >&2; exit 1; } -if [ -n "$LOCALE" ]; then - [ -f "$CLIENT/locale_$LOCALE.cfg" ] || { echo "$CLIENT/locale_$LOCALE.cfg not found" >&2; exit 1; } - pack="$(cut -d' ' -f3 < "$CLIENT/locale_$LOCALE.cfg" | tr -d '\r\n')" - tr -d '\r' < "$CLIENT/pack/Index" | grep -qxF "locale_$pack" || - { echo "locale_$pack is not registered in $CLIENT/pack/Index" >&2; exit 1; } -fi -TMP=/data/local/tmp/mt-client.tar -echo "packing $CLIENT ..." -tar -h -C "$(dirname "$CLIENT")" -cf - "$(basename "$CLIENT")" | adb shell "cat > $TMP" -echo "unpacking into $PKG files/ ..." -adb shell "run-as $PKG sh -c 'mkdir -p files && rm -rf files/Client && cd files && tar -xf $TMP' && rm -f $TMP" -if [ -n "$LOCALE" ]; then - adb shell "run-as $PKG sh -c 'cp files/Client/locale_$LOCALE.cfg files/Client/locale.cfg'" - echo "locale.cfg: $(adb shell "run-as $PKG cat files/Client/locale.cfg")" -fi -adb shell "run-as $PKG ls files/Client/pack/Index" >/dev/null && echo "done: files/Client" +. android/adb_env.sh +adb_device +exec python3 android/sync_client.py "$@" diff --git a/android/sync_client.py b/android/sync_client.py new file mode 100644 index 00000000..709cdc13 --- /dev/null +++ b/android/sync_client.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Bring the game files in the app's private files/Client (SDL_GetPrefPath, where src/host looks for +files/Client/pack/Index) up to date with a local client directory, sending only what changed. + +The device keeps files/Client/.mt_sync, the list (size, mtime, path) of what the last sync wrote. A sync +compares it with the local files, streams the new and changed ones as one tar through `adb shell -T run-as` +(no copy in /data/local/tmp), removes the ones gone locally, then writes the new list. Only the game +resources are synced (pack/, sound/, BGM/, miles/, lib/, channel.inf, locale*.cfg); what the app writes +there itself (settings such as ui_height.cfg, perf logs) is never touched. mouse.cfg is only seeded on +the first sync. + +Needs the debug APK installed (run-as). adb and the device come from android/adb_env.sh ($ADB, +$ANDROID_SERIAL); android/push-client.sh is the shell entry point. +""" + +import argparse +import hashlib +import os +import subprocess +import sys +import tarfile +import time +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +PKG = "org.metin2port.client" +MANIFEST = ".mt_sync" +HEADER = "# mt-sync 1" +SYNC_DIRS = ("pack", "sound", "BGM", "miles", "lib") +SEED_FILES = ("mouse.cfg",) +ZH_OUTPUT = REPO / "build/zh-locale/Client" +ZH_STAMP = REPO / "build/zh-locale/.inputs.sha256" + + +def adb(*args, **kwargs): + return subprocess.run([os.environ.get("ADB", "adb"), *args], **kwargs) + + +def run_as_command(script): + # `adb shell -T` (shell protocol v2, Android 7+): no pty, so stdin/stdout stay binary-safe, and unlike + # exec-in/exec-out it returns the remote exit status. + return [os.environ.get("ADB", "adb"), "shell", "-T", f"run-as {PKG} sh -c '{script}'"] + + +def run_as(script, **kwargs): + """Runs script with sh as the app user in its data directory.""" + return subprocess.run(run_as_command(script), **kwargs) + + +def zh_inputs_digest(client_base): + h = hashlib.sha256() + inputs = [REPO / "scripts/build_zh_locale.py", client_base / "pack/Index"] + inputs += sorted(p for p in (REPO / "data/zh_40250").rglob("*") if p.is_file()) + for path in inputs: + h.update(str(path.relative_to(REPO)).encode()) + h.update(path.read_bytes() if path.exists() else b"") + return h.hexdigest() + + +def ensure_zh_locale(): + """Rebuilds build/zh-locale/Client when its inputs (translations, the builder, the base pack Index) + changed since the last build.""" + digest = zh_inputs_digest(REPO / "Client") + built = (ZH_OUTPUT / "pack/Index").is_file() + if built and ZH_STAMP.is_file() and ZH_STAMP.read_text().strip() == digest: + return + print("zh locale: inputs changed, rebuilding build/zh-locale/Client ...") + result = subprocess.run([sys.executable, str(REPO / "scripts/build_zh_locale.py")], cwd=REPO) + if result.returncode == 0: + ZH_STAMP.write_text(digest + "\n") + elif built: + print("zh locale: rebuild failed, syncing the previous build", file=sys.stderr) + else: + sys.exit("zh locale: build failed (scripts/build_zh_locale.py)") + + +def local_files(client, locale): + """{device path: local path} of the synced resources; locale.cfg comes from locale_.cfg.""" + files = {} + for top in SYNC_DIRS: + root = client / top + if not root.is_dir(): + continue + for dirpath, dirnames, filenames in os.walk(root, followlinks=True): + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + for name in filenames: + if not name.startswith("."): + path = Path(dirpath) / name + files[path.relative_to(client).as_posix()] = path + for path in client.iterdir(): + if path.is_file() and (path.name == "channel.inf" or (path.name.startswith("locale") and path.suffix == ".cfg")): + files[path.name] = path + if locale: + cfg = client / f"locale_{locale}.cfg" + if not cfg.is_file(): + sys.exit(f"{cfg} not found") + pack = cfg.read_text().split()[2] + index = (client / "pack/Index").read_text(errors="replace").split() + if f"locale_{pack}" not in index: + sys.exit(f"locale_{pack} is not registered in {client}/pack/Index") + files["locale.cfg"] = cfg + return files + + +def stat_key(path): + st = path.stat() + return f"{st.st_size}\t{st.st_mtime_ns}" + + +def read_device_manifest(): + out = run_as(f"cat files/Client/{MANIFEST} 2>/dev/null", capture_output=True).stdout.decode(errors="replace") + lines = out.splitlines() + if not lines or lines[0] != HEADER: + return None + manifest = {} + for line in lines[1:]: + size, mtime, path = line.split("\t", 2) + manifest[path] = f"{size}\t{mtime}" + return manifest + + +class Progress: + def __init__(self, total): + self.total, self.done, self.last, self.start = total, 0, 0.0, time.monotonic() + + def add(self, n): + self.done += n + now = time.monotonic() + if now - self.last >= 1 or self.done == self.total: + self.last = now + rate = self.done / max(now - self.start, 1e-3) / 2**20 + print(f"\r {self.done / 2**20:8.1f} / {self.total / 2**20:.1f} MB {rate:5.1f} MB/s", end="", flush=True) + + +def push(files): + """Streams {device path: local path} into files/Client as one tar.""" + total = sum(p.stat().st_size for p in files.values()) + progress = Progress(total) + proc = subprocess.Popen(run_as_command("mkdir -p files/Client && cd files/Client && tar -xf -"), + stdin=subprocess.PIPE) + with tarfile.open(fileobj=proc.stdin, mode="w|", format=tarfile.GNU_FORMAT, dereference=True) as tar: + for name, path in sorted(files.items()): + info = tar.gettarinfo(str(path), arcname=name) # dereference: the zh client links the original packs + info.uid = info.gid = 0 + info.uname = info.gname = "" + with open(path, "rb") as f: + tar.addfile(info, f) + progress.add(info.size) + proc.stdin.close() + if progress.total: + print() + if proc.wait() != 0: + sys.exit("push failed (tar on the device)") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("client", nargs="?", type=Path, help="client directory (default: Client, or the zh build for --locale zh)") + parser.add_argument("--locale", help="write locale.cfg from locale_.cfg; zh uses build/zh-locale/Client, rebuilt when stale") + parser.add_argument("--full", action="store_true", help="delete the synced resources on the device and push everything") + parser.add_argument("--dry-run", action="store_true", help="only print what would change") + args = parser.parse_args() + + if args.locale == "zh" and args.client is None: + ensure_zh_locale() + client = ZH_OUTPUT + else: + client = args.client or REPO / "Client" + client = client.absolute() + if not (client / "pack/Index").is_file(): + sys.exit(f"{client}/pack/Index not found") + + if not adb("shell", "pm", "path", PKG, capture_output=True).stdout.strip(): + sys.exit(f"{PKG} is not installed (android/build.sh installs it)") + if run_as("true", capture_output=True).returncode != 0: + sys.exit(f"run-as {PKG} failed: the installed APK must be the debug build") + + files = local_files(client, args.locale) + wanted = {name: stat_key(path) for name, path in files.items()} + device = None if args.full else read_device_manifest() + first = device is None + if first: + changed = sorted(wanted) + removed = [] + # First sync (or --full): also seed the default settings the app would otherwise lack. + for name in SEED_FILES: + if (client / name).is_file(): + files[name] = client / name + changed.append(name) + else: + changed = sorted(n for n, key in wanted.items() if device.get(n) != key) + removed = sorted(n for n in device if n not in wanted) + size = sum(files[n].stat().st_size for n in changed) + print(f"sync {client} -> {PKG} files/Client: {len(changed)} files to push ({size / 2**20:.1f} MB), " + f"{len(removed)} to delete, {len(wanted) - len(changed) if not first else 0} unchanged" + + (" [full]" if args.full else " [first sync]" if first else "")) + if not changed and not removed: + return + if args.dry_run: + for n in changed[:50]: + print(" +", n) + for n in removed[:50]: + print(" -", n) + return + + if args.full: + dirs = " ".join(SYNC_DIRS) + run_as(f"mkdir -p files/Client && cd files/Client && rm -rf {dirs} {MANIFEST}", check=True) + # Drop the list before touching files: an interrupted sync then starts over as a first sync. + run_as(f"rm -f files/Client/{MANIFEST}", check=True) + if removed: + run_as('cd files/Client && while IFS= read -r f; do rm -f "$f"; done', + input="".join(n + "\n" for n in removed).encode(), check=True) + if changed: + push({n: files[n] for n in changed}) + manifest = HEADER + "\n" + "".join(f"{wanted[n]}\t{n}\n" for n in sorted(wanted)) + run_as(f"cat > files/Client/{MANIFEST}", input=manifest.encode(), check=True) + locale = run_as("cat files/Client/locale.cfg", capture_output=True).stdout.decode(errors="replace").strip() + print(f"done: files/Client updated (locale.cfg: {locale})") + + +if __name__ == "__main__": + main() diff --git a/docs/ANDROID-TESTING.md b/docs/ANDROID-TESTING.md index b7013f47..eec1125a 100644 --- a/docs/ANDROID-TESTING.md +++ b/docs/ANDROID-TESTING.md @@ -5,12 +5,20 @@ ## 安装和资源 ```sh -./android/build.sh Release --install -./android/push-client.sh /path/to/40250/Client -adb shell am start -n org.metin2port.client/.MainActivity +./android/build.sh [Debug|Release] [--launch] ``` -`android/push-client.sh` 将 40250 `Client` 目录复制到应用私有目录。APK 本身包含 `libmain.so`、`libSDL3.so`、着色器和 Python 2.7 标准库,不包含约 1.4 GB 的 40250 客户端资源。使用中文资源时运行 `./android/push-client.sh --locale zh`。 +构建完成后,如果有一台已授权的设备(多台时用 `ANDROID_SERIAL` 指定),脚本会: + +1. `adb install -r` 安装 APK;签名不一致(别的机器的 debug key)时卸载重装,应用数据随之清空。 +2. 运行 `android/push-client.sh --locale zh`,把资源同步到应用私有目录 `files/Client`。中文资源 `build/zh-locale/Client` 的输入(`data/zh_40250`、`scripts/build_zh_locale.py`、`Client/pack/Index`)有变化时先重新生成。 +3. 带 `--launch` 时唤醒手机并启动应用;锁屏状态下只提示解锁(锁屏启动会因 Vulkan surface 错误退出)。 + +没有设备时只构建 APK。其他选项:`--no-deploy` 只构建,`--no-push` 只安装,`--locale XX` 用 `Client/locale_XX.cfg`,`--full-push` 删除设备上的资源后全量重传。 + +资源同步是增量的:设备上的 `files/Client/.mt_sync` 记录上次写入的文件(大小、修改时间),之后只传新增和改动的文件,并删除本地已不存在的文件;第一次同步传完整的约 1.4 GB。只同步游戏资源(`pack/`、`sound/`、`BGM/`、`channel.inf`、`locale*.cfg`),应用自己写的设置和性能日志不会被覆盖或删除。单独同步资源:`./android/push-client.sh [--locale zh] [--full] [--dry-run] [CLIENT_DIR]`。 + +APK 本身包含 `libmain.so`、`libSDL3.so`、着色器和 Python 2.7 标准库,不包含 40250 客户端资源。 ## 连服务器 @@ -19,6 +27,6 @@ adb shell am start -n org.metin2port.client/.MainActivity \ --es args "--live-server HOST:AUTH_PORT:GAME_PORT --login-screen" ``` -默认不传参数时连接进程内假服。测试真服前确认模拟器/设备能访问相应主机与端口。用 `adb logcat` 查看崩溃和启动错误。`adb devices -l` 没有设备时,先连接模拟器或开启真机 USB 调试。 +从桌面图标启动(不传参数)时连接 `MainActivity.DEFAULT_ARGS` 里的真服登录界面;显式传入不含 `--live-server` 的参数时运行进程内假服。测试真服前确认模拟器/设备能访问相应主机与端口。用 `adb logcat` 查看崩溃和启动错误。`adb devices -l` 没有设备时,先连接模拟器或开启真机 USB 调试。 更详细的构建依赖与命令见 [src/host/README.md](../src/host/README.md)。 diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index b72a345e..48d2f694 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -204,8 +204,8 @@ PORT 扩展(40250 没有对应物,只为移动端/平台而加;不能改 4 - macOS:`brew install cmake sdl3 vulkan-headers vulkan-loader molten-vk shaderc`;构建见根目录 README (`-DMT_BUILD_NATIVE_RENDER=ON`,目标 `mt_native_render`)。 - Android:NDK 27.2(`ANDROID_NDK_HOME` 指向它)、openjdk@21、Android SDK;`adb` 用 `/opt/homebrew/bin/adb`。 - `./android/build.sh Release --install` 构建安装 APK,`./android/push-client.sh [--locale zh] ` 推送资源 - (APK 只含代码,资源在应用数据目录)。启动前先唤醒手机。 + `./android/build.sh [--launch]` 构建 APK,连着手机时自动安装并增量同步中文资源(`android/push-client.sh`; + APK 只含代码,资源在应用数据目录)。启动前先唤醒手机。 - Python 3:用于 `port_map.py` 和其他审计脚本。Python 2.7.18 只在复核 `py_embed_spike.py` 时需要。 - 真服账号、密码不写进仓库或 audit 账本。 diff --git a/docs/ZH-LOCALE.md b/docs/ZH-LOCALE.md index 8cc7a173..151392fe 100644 --- a/docs/ZH-LOCALE.md +++ b/docs/ZH-LOCALE.md @@ -34,7 +34,7 @@ build/native/src/host/mt_native_render \ --live-client build/zh-locale/Client --login-screen --width 1024 --height 768 ``` -Android:`./android/push-client.sh --locale zh`(默认推送 `build/zh-locale/Client`,符号链接按 +Android:`./android/build.sh` 连着手机时自动执行 `./android/push-client.sh --locale zh`(默认推送 `build/zh-locale/Client`,输入变化时先重新生成,符号链接按 目标文件复制,并以 `locale_zh.cfg` 写入设备上的 `locale.cfg`)。 物品与怪物原型从 40250 原版解压,按线上 40250 服务端 `gb2312name` 的 VNUM 快照 diff --git a/src/host/README.md b/src/host/README.md index 4f16689c..01ea2f30 100644 --- a/src/host/README.md +++ b/src/host/README.md @@ -23,7 +23,7 @@ returned by SDL's `SDL_GetBasePath`). `MT_PYTHON_STDLIB` can override the zip pa ## Android integration -`./android/build.sh [Debug|Release] [--install]` builds the arm64 APK +`./android/build.sh [Debug|Release] [--no-deploy] [--no-push] [--locale XX] [--full-push] [--launch]` builds the arm64 APK (`android/app/build/outputs/apk/debug/app-debug.apk`, package `org.metin2port.client`): it downloads SDL 3.4.16 (sha256-checked) into `build/android/_deps`, builds `libSDL3.so`, cross-builds `libmain.so` @@ -32,10 +32,15 @@ SDLActivity shell under `android/` (`MainActivity` passes the launch intent's `args` extra as argv). `mt_native_android_assets` supplies `native.vert.spv`, `native.frag.spv` and `python27.zip` as APK assets. -The 40250 `Client` directory is not packaged. `./android/push-client.sh` copies it -to the app's `files/Client` (`SDL_GetPrefPath`) with `run-as`, so it needs the -debug-signed APK installed first. Without arguments the app runs against the -in-process fake server; for a real server: +When a device is attached it then installs the APK and runs +`./android/push-client.sh --locale zh` (details in `docs/ANDROID-TESTING.md`). + +The 40250 `Client` directory is not packaged. `./android/push-client.sh` +(`android/sync_client.py`) syncs it into the app's `files/Client` (`SDL_GetPrefPath`) +with `run-as`, sending only the files changed since the last sync, so it needs the +debug-signed APK installed first. A launcher start opens the login screen of the server in +`MainActivity.DEFAULT_ARGS`; explicit args without `--live-server` run the in-process fake +server. To pick the server: ```sh adb shell am start -n org.metin2port.client/.MainActivity \