Merge branch 'feat/app-icon'

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-30 18:52:00 +09:00
co-authored by Claude Opus 5.5
30 changed files with 310 additions and 0 deletions
+2
View File
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.VIBRATE" />
<application android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:appCategory="game"
android:isGame="true"
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by branding/icon/build_icons.sh. -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by branding/icon/build_icons.sh. -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+106
View File
@@ -0,0 +1,106 @@
#!/bin/zsh
# Renders branding/icon/icon.svg into every platform icon and writes them where the builds pick them up:
# android/app/src/main/res/mipmap-* legacy ic_launcher / ic_launcher_round (API 24-25) and the
# adaptive-icon layers (API 26+, mipmap-anydpi-v26/*.xml)
# branding/icon/macos/AppIcon.icns macOS app icon (Big Sur squircle with drop shadow)
# branding/icon/ios/AppIcon.appiconset iOS single-size 1024 icon (opaque)
# The outputs are committed, so building the apps does not need this script. Rerun it after editing the SVG.
# Needs Google Chrome (SVG filters) and ImageMagick (`brew install imagemagick`); iconutil ships with macOS.
set -euo pipefail
here=${0:A:h}
repo=${here:h:h}
chrome=${CHROME:-"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"}
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
# icon.svg -> full / background-only / foreground-only (scaled into the adaptive-icon safe zone) SVGs.
python3 - "$here/icon.svg" "$work" <<'EOF'
import re, sys
src, out = sys.argv[1], sys.argv[2]
svg = open(src).read()
# Both layer groups sit at two-space indent; the foreground one is the last element and holds nested groups.
bg = re.search(r' <g id="background">.*?\n </g>\n', svg, re.S)
fg = re.search(r' <g id="foreground">.*\n </g>\n(?=</svg>)', svg, re.S)
assert bg and fg
open(out + "/full.svg", "w").write(svg)
open(out + "/background.svg", "w").write(svg.replace(fg.group(0), ""))
# Adaptive icons show the inner 72 of 108 dp (338 px of the 512 half-width) and launchers mask inside that.
# Scale the foreground so the ring (outer radius 405 around (512, 500)) sits at the same 79% of the visible
# radius as in the full icon: 405 * 0.66 = 267 = 0.79 * 338.
scaled = ' <g transform="translate(512 512) scale(0.66) translate(-512 -500)">\n' + fg.group(0) + ' </g>\n'
open(out + "/foreground.svg", "w").write(svg.replace(bg.group(0), "").replace(fg.group(0), scaled))
EOF
for name in full background foreground; do
"$chrome" --headless=new --disable-gpu --hide-scrollbars --default-background-color=00000000 \
--window-size=1024,1024 --screenshot="$work/$name.png" "file://$work/$name.svg" >/dev/null 2>&1
done
# Android
res=$repo/android/app/src/main/res
typeset -A legacy=(mdpi 48 hdpi 72 xhdpi 96 xxhdpi 144 xxxhdpi 192)
for density size in ${(kv)legacy}; do
dir=$res/mipmap-$density
mkdir -p $dir
layer=$(( size * 9 / 4 )) # 108 dp at this density
magick "$work/background.png" -resize ${layer}x${layer} "$dir/ic_launcher_background.png"
magick "$work/foreground.png" -resize ${layer}x${layer} "$dir/ic_launcher_foreground.png"
# Legacy launchers show the bitmap as is: a rounded square and a circle.
r=$(( size * 18 / 100 ))
magick "$work/full.png" -alpha set -resize ${size}x${size} \
\( -size ${size}x${size} xc:none -fill white -draw "roundrectangle 0,0 $((size-1)),$((size-1)) $r,$r" \) \
-compose DstIn -composite "$dir/ic_launcher.png"
magick "$work/full.png" -alpha set -resize ${size}x${size} \
\( -size ${size}x${size} xc:none -fill white -draw "circle $((size/2)),$((size/2)) $((size/2)),0" \) \
-compose DstIn -composite "$dir/ic_launcher_round.png"
done
mkdir -p $res/mipmap-anydpi-v26
for name in ic_launcher ic_launcher_round; do
cat > $res/mipmap-anydpi-v26/$name.xml <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by branding/icon/build_icons.sh. -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
EOF
done
# macOS: 824 px squircle-ish rounded rect (radius 185) centred on 1024 with a soft shadow, per the Big Sur grid.
magick "$work/full.png" -alpha set -resize 824x824 \
\( -size 824x824 xc:none -fill white -draw "roundrectangle 0,0 823,823 185,185" \) \
-compose DstIn -composite "$work/tile.png"
magick -size 1024x1024 xc:none \
\( "$work/tile.png" -background black -shadow 50x14+0+10 \) -gravity center -geometry +0+6 -compose Over -composite \
"$work/tile.png" -gravity center -geometry +0+0 -compose Over -composite "$work/mac1024.png"
iconset=$work/AppIcon.iconset
mkdir -p $iconset
for s in 16 32 128 256 512; do
magick "$work/mac1024.png" -resize ${s}x${s} "$iconset/icon_${s}x${s}.png"
magick "$work/mac1024.png" -resize $((s*2))x$((s*2)) "$iconset/icon_${s}x${s}@2x.png"
done
mkdir -p $here/macos
iconutil -c icns -o $here/macos/AppIcon.icns $iconset
cp "$work/mac1024.png" $here/macos/AppIcon-1024.png
# iOS: opaque, square; the system applies the mask.
ios=$here/ios/AppIcon.appiconset
mkdir -p $ios
magick "$work/full.png" -background '#12050a' -alpha remove -alpha off "$ios/AppIcon-1024.png"
cat > $ios/Contents.json <<'EOF'
{
"images" : [
{
"filename" : "AppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
EOF
echo "icons written"
+124
View File
@@ -0,0 +1,124 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
<!-- Metin2 client app icon: a Metin stone (the cursed monoliths the game is named after) with glowing
rune cracks, inside a gold ring over a blood-red dusk sky and ink mountains.
Full-bleed square; platforms apply their own mask. #background / #foreground are the Android
adaptive-icon layers. branding/icon/build_icons.sh renders every platform size from this file. -->
<defs>
<radialGradient id="sky" cx="50%" cy="42%" r="70%">
<stop offset="0" stop-color="#8e1b14"/>
<stop offset="0.45" stop-color="#4a0b0e"/>
<stop offset="1" stop-color="#12050a"/>
</radialGradient>
<radialGradient id="aura" cx="50%" cy="50%" r="50%">
<stop offset="0" stop-color="#ff7a2a" stop-opacity="0.85"/>
<stop offset="0.4" stop-color="#e2361a" stop-opacity="0.45"/>
<stop offset="1" stop-color="#e2361a" stop-opacity="0"/>
</radialGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fff0a8"/>
<stop offset="0.35" stop-color="#e8b64a"/>
<stop offset="0.7" stop-color="#a8701c"/>
<stop offset="1" stop-color="#f3cf6a"/>
</linearGradient>
<linearGradient id="faceL" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5b5566"/>
<stop offset="1" stop-color="#211d28"/>
</linearGradient>
<linearGradient id="faceR" x1="1" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2a2531"/>
<stop offset="1" stop-color="#0b090f"/>
</linearGradient>
<linearGradient id="faceM" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#46404f"/>
<stop offset="1" stop-color="#16131b"/>
</linearGradient>
<linearGradient id="mtnFar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#3a0c10"/>
<stop offset="1" stop-color="#1c0609"/>
</linearGradient>
<linearGradient id="mtnNear" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#1e070a"/>
<stop offset="1" stop-color="#080305"/>
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="9" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3"/>
</filter>
<filter id="shadow" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="10" stdDeviation="14" flood-color="#000" flood-opacity="0.7"/>
</filter>
</defs>
<g id="background">
<!-- sky -->
<rect width="1024" height="1024" fill="url(#sky)"/>
<!-- blood moon behind the stone -->
<circle cx="512" cy="430" r="250" fill="#c0301f" opacity="0.35"/>
<circle cx="512" cy="430" r="330" fill="url(#aura)"/>
<!-- ink mountains -->
<path d="M0 700 L110 610 L180 650 L280 540 L360 620 L420 590 L512 660 L610 575 L690 630 L760 540 L860 640 L940 600 L1024 660 L1024 1024 L0 1024 Z" fill="url(#mtnFar)"/>
<path d="M0 800 L90 740 L170 780 L260 710 L350 790 L512 760 L660 800 L760 720 L860 770 L950 730 L1024 770 L1024 1024 L0 1024 Z" fill="url(#mtnNear)"/>
</g>
<g id="foreground">
<!-- gold ring -->
<g filter="url(#shadow)">
<circle cx="512" cy="500" r="392" fill="none" stroke="url(#gold)" stroke-width="26"/>
<circle cx="512" cy="500" r="360" fill="none" stroke="#e8b64a" stroke-width="4" opacity="0.8"/>
<g id="stud"><circle cx="512" cy="108" r="17" fill="url(#gold)" stroke="#6b430c" stroke-width="3"/><circle cx="512" cy="108" r="6" fill="#c0301f"/></g>
<use href="#stud" transform="rotate(45 512 500)"/>
<use href="#stud" transform="rotate(90 512 500)"/>
<use href="#stud" transform="rotate(135 512 500)"/>
<use href="#stud" transform="rotate(225 512 500)"/>
<use href="#stud" transform="rotate(270 512 500)"/>
<use href="#stud" transform="rotate(315 512 500)"/>
</g>
<!-- the Metin stone -->
<g filter="url(#shadow)">
<!-- rocky base -->
<path d="M300 808 L360 752 L452 770 L512 742 L590 768 L668 750 L728 806 L690 840 L340 842 Z" fill="#17121a"/>
<!-- left face -->
<path d="M512 150 L408 300 L372 560 L400 790 L500 810 L486 520 Z" fill="url(#faceL)"/>
<!-- middle face -->
<path d="M512 150 L486 520 L500 810 L560 802 L566 470 Z" fill="url(#faceM)"/>
<!-- right face -->
<path d="M512 150 L566 470 L560 802 L632 780 L656 540 L616 290 Z" fill="url(#faceR)"/>
<!-- facet highlights -->
<path d="M512 150 L408 300 L372 560" fill="none" stroke="#9a92a8" stroke-width="4" opacity="0.7"/>
<path d="M512 150 L486 520 L500 810" fill="none" stroke="#6d6579" stroke-width="3" opacity="0.6"/>
<path d="M408 300 L452 250 L512 150 M372 560 L420 600 L400 790" fill="none" stroke="#7a7288" stroke-width="2.5" opacity="0.5"/>
<path d="M616 290 L656 540 L632 780" fill="none" stroke="#ff5a2a" stroke-width="5" opacity="0.75"/>
<path d="M566 470 L620 430 M560 802 L600 700" fill="none" stroke="#3b3444" stroke-width="2.5" opacity="0.8"/>
</g>
<!-- glowing cracks and runes -->
<g filter="url(#glow)" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M470 250 L452 330 L478 380 L440 470 L462 540 L430 640 L452 720" stroke="#ff6a1f" stroke-width="9"/>
<path d="M452 330 L418 360" stroke="#ff6a1f" stroke-width="6"/>
<path d="M440 470 L404 500" stroke="#ff6a1f" stroke-width="6"/>
<path d="M548 300 L566 380 L540 430 L584 520 L560 600 L596 690" stroke="#ff4a18" stroke-width="8"/>
<path d="M584 520 L620 540" stroke="#ff4a18" stroke-width="6"/>
<!-- rune on the middle face -->
<path d="M526 575 L556 630 L526 685 L496 630 Z M526 605 L526 655" stroke="#ffd27a" stroke-width="7"/>
</g>
<!-- hot cores of the cracks -->
<g stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#fff0c0" opacity="0.9">
<path d="M470 250 L452 330 L478 380 L440 470 L462 540 L430 640 L452 720" stroke-width="3"/>
<path d="M548 300 L566 380 L540 430 L584 520 L560 600 L596 690" stroke-width="3"/>
<path d="M526 575 L556 630 L526 685 L496 630 Z M526 605 L526 655" stroke-width="2.5"/>
</g>
<!-- embers -->
<g fill="#ffb25a" filter="url(#soft)">
<circle cx="340" cy="420" r="6"/><circle cx="690" cy="360" r="5"/><circle cx="660" cy="620" r="7"/>
<circle cx="360" cy="650" r="5"/><circle cx="600" cy="220" r="4"/><circle cx="410" cy="200" r="4"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "AppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.
+52
View File
@@ -0,0 +1,52 @@
#!/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.
set -euo pipefail
repo=${0:A:h:h}
build=${1:-$repo/build/release}
client=${2:-$repo/Client}
out=${3:-$build/Metin2.app}
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
#!/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
<?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