Improve macOS Retina UI clarity and bundle HD assets

This commit is contained in:
shen
2026-09-30 16:13:31 -07:00
parent 905f23f135
commit d1f8d3dddd
26 changed files with 505 additions and 134 deletions
+3 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Builds hd_ui.pak: the 40250 UI textures at twice their resolution, for src/host/hd_ui.cpp.
"""Builds hd_ui.pak: the 40250 UI textures at twice their resolution, for the native texture loader.
The phone lays the 40250 UI out 600 logical pixels high and magnifies it about 1.8x onto the screen, so
the 1x art (32x32 icons, board frames) looks soft. This reads every UI image the client would load
@@ -240,6 +240,8 @@ def main() -> int:
"-f", "png"], cwd=args.esrgan, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
with ProcessPoolExecutor() as pool:
items = [r for r in pool.map(finish, [(m, work) for m in metas], chunksize=16) if r]
if len(items) != len(metas):
sys.exit(f"incomplete upscale: {len(items)} of {len(metas)} images; archive was not replaced")
write_pak(out, items)
print(f"wrote {out}: {len(items)} images, {out.stat().st_size / 2**20:.1f} MB")
finally:
+76 -44
View File
@@ -1,52 +1,84 @@
#!/bin/zsh
# Wraps a built mt_native_render into a double-clickable Metin2.app with the branding icon:
# scripts/package_mac_app.sh [BUILD_DIR=build/release] [CLIENT_DIR=<repo>/Client] [OUT=BUILD_DIR/Metin2.app]
# The bundle is for this machine: the executable still links Homebrew's SDL3 / Vulkan loader / MoltenVK, and
# the launcher runs the live client from CLIENT_DIR (an absolute path baked in; MT_CLIENT_DIR overrides it
# at launch). The shaders and python27.zip go to Contents/Resources, which SDL_GetBasePath returns in a bundle.
# Portable local macOS bundle with private Client assets and the validated 2x UI archive.
# scripts/package_mac_app.sh [BUILD_DIR] [CLIENT_DIR] [OUT] [HD_UI_PAK]
set -euo pipefail
repo=${0:A:h:h}
build=${1:-$repo/build/release}
client=${2:-$repo/Client}
out=${3:-$build/Metin2.app}
client=${2:-$repo/build/zh-locale/Client}
out=${3:-$repo/dist/Metin2.app}
pak=${4:-${MT_HD_UI_PAK:-$repo/build/hd-ui/hd_ui.pak}}
bin=$build/src/host/mt_native_render
[[ -x $bin ]] || { echo "missing $bin (build the mt_native_render target first)" >&2; exit 1; }
client=${client:A}
version=$(git -C $repo describe --always --dirty 2>/dev/null || echo dev)
rm -rf $out
mkdir -p $out/Contents/MacOS $out/Contents/Resources
cp $bin $out/Contents/MacOS/
cp $build/src/host/native.vert.spv $build/src/host/native.frag.spv $build/src/host/python27.zip $out/Contents/Resources/
cp $repo/branding/icon/macos/AppIcon.icns $out/Contents/Resources/
cat > $out/Contents/MacOS/Metin2 <<EOF
[[ -x "$bin" ]] || { echo "missing native executable: $bin" >&2; exit 1; }
[[ -d "$client/pack" ]] || { echo "missing Client/pack: $client" >&2; exit 1; }
[[ -f "$pak" ]] || { echo "missing HD UI archive: $pak (run scripts/build_hd_ui.py first)" >&2; exit 1; }
python3 "$repo/scripts/verify_hd_ui.py" "$pak"
version=$(git -C "$repo" describe --always --dirty 2>/dev/null || echo dev)
mkdir -p "${out:A:h}"
stage=$(mktemp -d "${out:A}.tmp.XXXXXX")
trap 'rm -rf "$stage"' EXIT
mkdir -p "$stage/Contents/MacOS" "$stage/Contents/Resources" "$stage/Contents/Frameworks"
cp "$bin" "$stage/Contents/MacOS/mt_native_render"
cp "$build/src/host/native.vert.spv" "$build/src/host/native.frag.spv" "$build/src/host/python27.zip" "$stage/Contents/Resources/"
cp "$repo/branding/icon/macos/AppIcon.icns" "$stage/Contents/Resources/"
cp "$pak" "$stage/Contents/Resources/hd_ui.pak"
# Clone private assets on APFS; fall back to an ordinary copy on other filesystems.
cp -cR "$client" "$stage/Contents/Resources/Client" 2>/dev/null || ditto "$client" "$stage/Contents/Resources/Client"
for lib in libvulkan.1.dylib libSDL3.0.dylib; do
dependency=$(otool -L "$bin" | awk -v name="$lib" '$1 ~ name "$" {print $1; exit}')
[[ -f "$dependency" ]] || { echo "cannot resolve $lib: $dependency" >&2; exit 1; }
cp "$dependency" "$stage/Contents/Frameworks/$lib"
install_name_tool -change "$dependency" "@rpath/$lib" "$stage/Contents/MacOS/mt_native_render"
install_name_tool -id "@rpath/$lib" "$stage/Contents/Frameworks/$lib"
done
for rpath in ${(f)"$(otool -l "$bin" | awk '/LC_RPATH/ {getline; getline; print $2}')"}; do
install_name_tool -delete_rpath "$rpath" "$stage/Contents/MacOS/mt_native_render"
done
install_name_tool -add_rpath '@executable_path/../Frameworks' "$stage/Contents/MacOS/mt_native_render"
molten=${MT_MOLTENVK:-/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib}
[[ -f "$molten" ]] || molten=/usr/local/opt/molten-vk/lib/libMoltenVK.dylib
[[ -f "$molten" ]] || { echo "missing MoltenVK (set MT_MOLTENVK)" >&2; exit 1; }
cp "$molten" "$stage/Contents/Frameworks/libMoltenVK.dylib"
cat > "$stage/Contents/Resources/MoltenVK_icd.json" <<'JSON'
{"file_format_version":"1.0.0","ICD":{"library_path":"../Frameworks/libMoltenVK.dylib","api_version":"1.2.0","is_portability_driver":true}}
JSON
cat > "$stage/Contents/MacOS/Metin2" <<'SH'
#!/bin/sh
# Finder starts apps with / as the working directory; the client reads its packs relative to its directory.
cd "\${MT_CLIENT_DIR:-$client}" || exit 1
exec "\$(dirname "\$0")/mt_native_render" --live-client . "\$@"
EOF
chmod +x $out/Contents/MacOS/Metin2
cat > $out/Contents/Info.plist <<EOF
set -eu
CONTENTS_DIR="$(cd "$(dirname "$0")/.." && pwd -P)"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
export VK_ICD_FILENAMES="$RESOURCES_DIR/MoltenVK_icd.json"
# Resource lookup also works when Finder starts us with cwd=/ or Client is overridden.
: "${MT_HD_UI_PAK:=$RESOURCES_DIR/hd_ui.pak}"
export MT_HD_UI_PAK
cd "$RESOURCES_DIR"
exec "$CONTENTS_DIR/MacOS/mt_native_render" --interactive \
--live-client "${MT_CLIENT_DIR:-$RESOURCES_DIR/Client}" \
--fake-mobs 24 --width 1280 --height 800 "$@"
SH
chmod +x "$stage/Contents/MacOS/Metin2"
cat > "$stage/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>Metin2</string>
<key>CFBundleDisplayName</key><string>Metin2</string>
<key>CFBundleIdentifier</key><string>org.metin2port.client</string>
<key>CFBundleExecutable</key><string>Metin2</string>
<key>CFBundleIconFile</key><string>AppIcon</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1</string>
<key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>12.0</string>
<key>LSApplicationCategoryType</key><string>public.app-category.role-playing-games</string>
<key>NSHighResolutionCapable</key><true/>
</dict>
</plist>
EOF
# Ad-hoc signature so Gatekeeper on Apple silicon runs the copied binary.
codesign --force --deep --sign - $out >/dev/null 2>&1 || true
echo $out
<plist version="1.0"><dict>
<key>CFBundleName</key><string>Metin2</string>
<key>CFBundleDisplayName</key><string>Metin2</string>
<key>CFBundleIdentifier</key><string>org.metin2port.client</string>
<key>CFBundleExecutable</key><string>Metin2</string>
<key>CFBundleIconFile</key><string>AppIcon</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1</string>
<key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>12.0</string>
<key>LSApplicationCategoryType</key><string>public.app-category.role-playing-games</string>
<key>NSHighResolutionCapable</key><true/>
</dict></plist>
PLIST
for library in "$stage"/Contents/Frameworks/*.dylib; do codesign --force --sign - "$library"; done
codesign --force --sign - "$stage/Contents/MacOS/mt_native_render"
codesign --force --deep --sign - "$stage"
codesign --verify --deep --strict "$stage"
# Publish only after validating the complete replacement bundle.
rm -rf "$out"
mv "$stage" "$out"
trap - EXIT
echo "$out"
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Validate a complete MTHD archive before including private HD art in a mac bundle."""
import pathlib
import struct
import sys
def verify(path):
data = pathlib.Path(path).read_bytes()
if len(data) < 12 or data[:4] != b'MTHD':
raise ValueError('invalid MTHD header')
version, count = struct.unpack_from('<II', data, 4)
if version != 1 or not 0 < count <= 100000:
raise ValueError('invalid archive version/count')
index, entries, names = 12, [], set()
for _ in range(count):
if index + 2 > len(data): raise ValueError('truncated index')
size, = struct.unpack_from('<H', data, index)
index += 2
if not 0 < size <= 4096 or index + size + 8 > len(data): raise ValueError('invalid entry')
name = data[index:index+size].decode('latin1')
index += size
offset, length = struct.unpack_from('<II', data, index)
index += 8
if name in names or not length or offset + length > len(data): raise ValueError('invalid payload range')
names.add(name)
entries.append((name, offset, length))
for name, offset, length in entries:
image = data[offset:offset+length]
if offset < index or length < 33 or image[:8] != b'\x89PNG\r\n\x1a\n' or image[12:16] != b'IHDR':
raise ValueError('invalid PNG: ' + name)
w, h = struct.unpack_from('>II', image, 16)
if not 0 < w <= 4096 or not 0 < h <= 4096: raise ValueError('invalid dimensions: ' + name)
return count
if __name__ == '__main__':
try:
print(f'HD UI archive valid: {verify(sys.argv[1])} textures')
except (ValueError, OSError, IndexError, struct.error) as error:
sys.exit(str(error))