Improve macOS Retina UI clarity and bundle HD assets
This commit is contained in:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user