makepad/apps/arcade/download_assets.sh
Admin 0ac21bdc9d A real cast: 36 rigged characters across 3 shared rigs — and Kenney IS rigged
THE FINDING THAT CHANGES THE PREMISE: "1 rigged model in 4,442" was measured
with the BROKEN GLB probe (the gLUF magic bug). With the probe fixed the
library holds 36 rigged models across three rigs:
  41 joints — 9 KayKit heroes + undead, up to 95 clips
   7 joints — 22 KENNEY civilians (male/female a-f, orc, human, archer, shop
              employees, skaters, soldiers), 32 clips
   6 joints — 5 Kenney platformer characters, 25 clips
So a village can be populated with 22 visually distinct civilians TODAY, with
no third-party pack at all. Every conclusion drawn from that probe before it
was fixed needs re-checking, not just this one.

KayKit: 9 characters fetched (Adventurers + Skeletons), pinned by commit +
sha256, 37 MB, gitignored. CC0 verified by READING LICENSE.txt at each pinned
commit, recorded in the script header and CREDITS.toml.

THE SHARED RIG HOLDS ACROSS PACKS, proven rather than assumed: hashing the
joint-name list of all nine files yields the SAME digest — 41 joints, same
names, same order — despite two separate repositories. Skeleton clips are a
strict superset (95 = the adventurers' 76 + 19 undead extras: awaken,
resurrect, spawn, taunt). So a clip authored for the knight plays on the
skeleton warrior and one animation path drives the cast. A test pins this,
including that both packs are present, so a version bump cannot silently break
it.

The texture trap that cost the Kenney fetch three attempts does NOT apply:
KayKit GLBs EMBED their atlas (image/png in a bufferView), verified by parsing
all nine.

tests/rigged.rs parses all 36 rigged models through makepad_game_render::skin
— the loader the app actually runs — and asserts the index's joint and clip
counts match it. Deliberate: the index's own probe was wrong for the entire
library once and survived because the fixture encoded the same error.

find_cast groups by JOINT COUNT rather than pack, because the valuable fact is
cross-pack interchangeability. Cast states are the INTERSECTION, not the union
— advertising a state one member cannot perform is worse than a shorter list.
Added the state words the skeletons needed (spawn/resurrect/taunt/use):
Skeletons_Awaken_Floor previously matched nothing, so "an undead that rises
from the ground" was unfindable.

One bug found in its own work: casts_to_json emitted a doubled closing brace —
malformed JSON that still looked fine in a log. Fixed with a structural test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 11:43:42 +02:00

448 lines
23 KiB
Bash
Executable file

#!/usr/bin/env bash
# Fetch the CC0 model packs Makepad Arcade uses for its stock asset library.
# Nothing here is vendored in git (see resources/*/.gitignore) — run this once
# after checkout. Every file is pinned to an exact upstream commit and verified
# by sha256, so a moved or tampered upstream fails loudly instead of silently
# changing the library.
#
# ./download_assets.sh fetch everything (idempotent)
# ./download_assets.sh --list show packs, counts and licences, download nothing
#
# ----------------------------------------------------------------------------
# CREDITS — all assets below are CC0 (public domain). Attribution is not
# required by the licence; we credit anyway, because these libraries exist
# because someone chose to give them away.
#
# Kenney — https://kenney.nl/assets
# Vehicles, city, arena, platformer and FPS kits. Thank you @KenneyNL.
#
# KayKit / Kay Lousberg — https://kaylousberg.itch.io/
# Rigged + animated characters: 5 adventurers (Knight, Barbarian, Mage,
# Rogue, Rogue_Hooded) and 4 skeletons (Warrior, Mage, Rogue, Minion).
# All nine share one 41-joint rig. CC0 verified in each pack's LICENSE.txt
# at the pinned commit. Thank you Kay.
#
# NOTE ON AUDIO FORMAT: every Kenney audio pack ships Ogg Vorbis only (no WAV
# variant exists upstream — checked all seven packs). This tree has no vorbis
# decoder, so the sounds are downloaded and indexed (searchable by the AI) but
# not yet playable; a decoder, or an opt-in local transcode, is the missing
# piece. `--transcode` below does the latter when ffmpeg is installed.
# ----------------------------------------------------------------------------
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resources"
MODELS="$ROOT/models/kenney"
CHARS="$ROOT/characters"
AUDIO="$ROOT/audio/kenney"
MANIFEST="$ROOT/MIRROR.toml"
# ---------------------------------------------------------------------------
# 3D model packs (the full Kenney 3D catalogue) are described by
# resources/MIRROR.toml rather than inlined here: 50 packs is too many to keep
# readable in shell, and that file is also what makes an independent mirror
# reproducible.
#
# Source selection:
# ARCADE_ASSET_MIRROR=https://our.host/path or --mirror=<base>
# A mirror is expected to serve <base>/<slug>.zip. Whichever host serves the
# bytes, the sha256 from MIRROR.toml is verified identically — a mirror is
# never trusted more than upstream.
#
# Selection:
# (default) the "core" packs — ~1900 models, ~60 MB
# --packs=all every usable pack — 4669 models, ~166 MB
# --packs=a,b,c named packs
# ---------------------------------------------------------------------------
MIRROR="${ARCADE_ASSET_MIRROR:-}"
PACKSEL="core"
fetched=0
cached=0
# Kenney audio packs, pinned by content-hashed download URL.
# Each entry: <pack>|<url-hash-segment>|<sha256 of zip>|<sound count>
KENNEY_AUDIO=(
"impact-sounds|87b4ddecda-1677589768|029d734af1582474edf3a694d1b0cebc97c1c152f2f39fa34d4c2bafc5de77f8|130"
"interface-sounds|fa43c1dd4d-1677589452|f2193d072726d6758a5f7871b2dcc54dcce0d5c35c6f0a62f92549b327c81232|100"
"sci-fi-sounds|6b296f9ecf-1677589334|119340f351a5098ad814f78719438c0da355a9ce8a4c8a3af6a8d48aa3d49e04|73"
"music-jingles|f37e530b9e-1677590399|b729ba57959bd58793d2c5cafa348aaf2655d354f3da35ec4729e03ec77197b8|86"
"ui-audio|490d233f68-1677590494|946fc23a63d535d693eb31b2eabb80c8c28d6351e2186b344ceb71b2cb1d5eb6|52"
"rpg-audio|8e99002d76-1677590336|6dbeaf8544da958d8f2adcb4a4a4b76c1ade34a05f8ab9edccd327da7375f38b|52"
"digital-audio|216eac4753-1677590265|24e6ce28b76a6d8c89cff4d331e0965ff5c3de8a73c612028e9d363cc64e4f06|63"
)
# Kenney starter kits, pinned. Each entry: <pack> <repo> <commit> <subdir>
KENNEY_PACKS=(
"arena|Starter-Kit-Basic-Scene|a6927e66ff8dd8e173660ce4825abe773c65f683|sample/Mini Arena/Models/GLB format"
"city|Starter-Kit-City-Builder|4535092b740b378b700efd9df9e27a631815b84a|models"
"fps|Starter-Kit-FPS|185fd2326d74a5cf858cffc616f87cf9696f9cc0|models"
"platformer|Starter-Kit-3D-Platformer|3fa8a04b1c01ab23db43123d4ce814a34c3fc7f0|models"
"racing|Starter-Kit-Racing|f5241ebdf00c25bc951bf4fdb7950bb1b78b4bcc|models"
)
# KayKit character packs, pinned. CC0 verified in each repo's LICENSE.txt at
# the pinned commit ("License: (Creative Commons Zero, CC0)").
#
# ALL NINE CHARACTERS SHARE ONE RIG — 41 joints, identical names in identical
# order, verified by hashing the joint-name list of every file (both packs
# produce the same digest). So one animation-driving code path serves the whole
# cast, and a clip authored against any of them plays on all of them.
# Adventurers ship 76 clips; skeletons ship those same 76 plus 19 undead extras
# (awaken, resurrect, spawn, taunt) — a strict superset, no clip is lost.
#
# The GLBs EMBED their texture (image/png in a bufferView), so unlike the Kenney
# packs there is no external URI to resolve and no path to get wrong. The
# sidecar *_texture.png files are fetched anyway because the skin loader
# deliberately ignores materials ("the caller binds its own texture"), so the
# app supplies the atlas itself.
KAYKIT_ADV_COMMIT="672074b73ba276876a19e8816ecdc5241817ab47"
KAYKIT_ADV_BASE="https://raw.githubusercontent.com/KayKit-Game-Assets/KayKit-Character-Pack-Adventures-1.0/$KAYKIT_ADV_COMMIT"
KAYKIT_ADV_DIR="addons/kaykit_character_pack_adventures/Characters/gltf"
KAYKIT_SKEL_COMMIT="15b62b9bad122f72926c10fb14d622c73819fa54"
KAYKIT_SKEL_BASE="https://raw.githubusercontent.com/KayKit-Game-Assets/KayKit-Character-Pack-Skeletons-1.0/$KAYKIT_SKEL_COMMIT"
KAYKIT_SKEL_DIR="addons/kaykit_character_pack_skeletons/Characters/gltf"
# <local-name>|<pack>|<upstream file>|<sha256>|<texture local-name>
# Rogue_Hooded has no atlas of its own upstream — it re-skins rogue_texture.
KAYKIT_CHARS=(
"knight|adv|Knight.glb|60428e3abc09ba83e595d256e3af8c5c976b46cdae599f0802fc82b4a3445168|knight_texture.png"
"barbarian|adv|Barbarian.glb|cefc311a0e10c7858b6141f5ada7e33268727564fb8ac1347aab97d000669cc6|barbarian_texture.png"
"mage|adv|Mage.glb|cf898585da33fab50c724d31605fb931eb2912e6d2280092141e98ca81ad507d|mage_texture.png"
"rogue|adv|Rogue.glb|e825437cd4d2ee9c1960b517a74a69101e33eb409ae7fa8cedc7134a998fbb7d|rogue_texture.png"
"rogue_hooded|adv|Rogue_Hooded.glb|93e6e25213009952276d9cf34f5d96a243767334c66f280db0433ddfabb91545|rogue_texture.png"
"skeleton_warrior|skel|Skeleton_Warrior.glb|178b6fda810b814c250d8a2010c24dfd9b458b9006dd323353e620b7ff118bbe|skeleton_texture.png"
"skeleton_mage|skel|Skeleton_Mage.glb|e05b0f5cfa395271c9f75fd07c0a0613c56f401ece4ab644e080001a79971075|skeleton_texture.png"
"skeleton_rogue|skel|Skeleton_Rogue.glb|4003f2b77891bb56f7e0de7d555abcb497aebbcaee35b614211f16275f9ccae3|skeleton_texture.png"
"skeleton_minion|skel|Skeleton_Minion.glb|6ffc003f895bed0b074791e0e490846210a2e2f8fc7da300aba53cc185f95968|skeleton_texture.png"
)
# <local-name>|<pack>|<upstream file>|<sha256>
KAYKIT_TEXTURES=(
"knight_texture.png|adv|knight_texture.png|5d250ccc5da020e6126bfa3839f83bd9a465a951ed223e4d13c08b1925e154d4"
"barbarian_texture.png|adv|barbarian_texture.png|7329b2ff9709e8d54c886d5ef49c08bd42b12be3bbb2facb1488a6b48b1e8a80"
"mage_texture.png|adv|mage_texture.png|ea49f094b960402635fe51db9f1864960c97271b17f2e3554e5aca1b2bbba144"
"rogue_texture.png|adv|rogue_texture.png|a4032e877c3b91939f5cdbb630349c1998fdbc3211bbd587c111125500fe4cc5"
"skeleton_texture.png|skel|skeleton_texture.png|15741a25c53e04fa9bf3beac3bc0de442359404b1ff9be863b892cb551ad3657"
)
kaykit_base() { case "$1" in adv) echo "$KAYKIT_ADV_BASE/$KAYKIT_ADV_DIR" ;; skel) echo "$KAYKIT_SKEL_BASE/$KAYKIT_SKEL_DIR" ;; esac; }
# Per-pack file manifest: "<filename> <sha256>" lines.
pack_files() {
case "$1" in
arena)
echo "banner.glb 96e1cf7924fcc871be0e7e182c9b918de381b9783ca9e96922f8db62af384056"
echo "block.glb 656e07f0a3e7d0620892aca6fc40683b4266ff6b9672ecf89f3553e5ed85a1fb"
echo "border-corner.glb 120ce330ca50fe50e51bf693eadb957abaca972c42d573a07d8e8e404560bc3c"
echo "border-straight.glb 76ef69e8858f3d17c652d92033a553db58296135a5810d1bd5e54f47b5aa40c4"
echo "bricks.glb fd1d2c033c7db8b7352ac1becdab447d09cabcc11c73adf9bb6790f9680eaa86"
echo "character-soldier.glb b0410b4a34068e56ac1af3a2571f4497f84f1e481cced895b618d1fe8136529c"
echo "column-damaged.glb 442a6856e7e2d042c9e3af155dabf6d394f7ba91b2e271ee6392da22555a072c"
echo "column.glb 5c815b5fd43d613a21afddaaa90dc2f90b80e31f03dbdde98921cb7424e836c0"
echo "floor-detail.glb 804224caf3008fcdbd57d7ccc2c4734320c58ca880895699307670d2443e324b"
echo "floor.glb 15512c5ca6e9d3eb60efd237bbbf5b8010fdea63e0d8e6623f72b59de20279ab"
echo "stairs-corner-inner.glb 2e232cecda1d633acc1b78707ee50089fd40a167293c173536d61ef064697a5c"
echo "stairs-corner.glb af8eb87dc8826e184f585ebccf0592c909da7370559cd840b218593f4b1c8a38"
echo "stairs.glb 54a3f12c720f9cb83027b5429e330d7c50578f70771fd1bf9a2e521fe131ca6f"
echo "statue.glb 5ba8ecba963f6b3ec974809e2bf0b615da7e2701cd0efc6a3c0e946efb147397"
echo "tree.glb f8632c9758a78f60aa4ca68bf0e5ac972770167a5608e179293a12552d8fc2ef"
echo "trophy.glb 253f6d5f321ab20f8f505dfe35b7404c98d2d492728e6df5f69dd29e936b8be5"
echo "wall-corner.glb 4dc0e770106a122885f41be0bb4024466a7f58bc29a13f07b6600c4babbeb777"
echo "wall-gate.glb f70a86344f202b3a8057e3a6ee9cd632ba51a5c16795cfde34cd10e906f14697"
echo "wall.glb b05ab8645136cc26e761ae2189f660a585dca88c11721771bca3fb4c1f7a807f"
echo "weapon-rack.glb bb95da3746098e36f3d87a1e1036ca70ead7fe95860191871e02f7c58ff1efe5"
echo "weapon-spear.glb 9634e12abce01a07d40d5156454bfa1123eba72af6449fccf577980e331adbeb"
echo "weapon-sword.glb e4d2219d954148dd8b04fccc7602726c34eafad58c0dc5a45268155fcf6cc8c4"
;;
city)
echo "building-garage.glb 7373b558fc9b1e27e60b60ca1ce53000af7e132d7304eeb6d49b47528a3b6806"
echo "building-small-a.glb 22ce989013bd16b1732e81798e343cf85f947e92b6c93825b18131feded48e07"
echo "building-small-b.glb 0bc8459045975158524753d6b648ff77e5f91fe8c033bc4886930b6d55543a2b"
echo "building-small-c.glb 3ea0f46fbed4e0acba7fda365cd968b3f23a6e02b9fee79dd54c99c42ffadf2f"
echo "building-small-d.glb 0ab476fbd7956cea52e590a8e68e351ce2f6fc04df158aa3f5a76b7744bf37a0"
echo "grass-trees-tall.glb d23e3b722453236bf6f4f8922b7bf64f81a7f3f804848a89c4b100ccbf13abef"
echo "grass-trees.glb cb06d03cc1c64ca7e7692c0aef3628d8e7cd4afee69093025f9ddd668653a4ce"
echo "grass.glb 3e3ec91132ad8519967aa2e4c0bedbab2aeed39b5714d66068bb75648ae73a52"
echo "pavement-fountain.glb ea2996089e90a79ba13d764106d374486bb8137666cf1cdbaebd1927ef365185"
echo "pavement.glb 60152776325d436761a13eb0c2ec9368b393bc82cf754b63cab810d79639ade9"
echo "road-corner.glb 85aec60d66c5084bb658274a1468139a5def1e9e9575c01286ed6c2069916789"
echo "road-intersection.glb 0212ffe9945b933535503372855c54dee20d54ddfc07e1c72963e67558a14183"
echo "road-split.glb 31bf582953db8315edfade4ba49d81145a78bda9757b6442dc417e14cc84b5ff"
echo "road-straight-lightposts.glb fc5340621fefdd43ab0bbf4bdaa41de9822dc545ce787fbfd92b75ce85d51e62"
echo "road-straight.glb 008a6305de778439d1a99be78a3e0945c72a9bc9cc0bade1b51a666d5db01d0b"
;;
fps)
echo "blaster-repeater.glb e76220e5ad3877d879e70fe21bd5bc76a5988b8682aa5f7976739e71caf70e05"
echo "blaster.glb 9c2110d94c1bd7e01bfe6827a3d87e14962a1f721d8b300c67bdd36f492140a1"
echo "cloud.glb 4c667239958ae8950b6bc2f55bdb59814b24fc9f6d8531eb09411150e09c87f2"
echo "enemy-flying.glb e4868f6d8cac2a8f728430229789206841ca64727d94de2d808b468e4f182f8c"
echo "grass-small.glb 8dfe00761fa833daab3192148b9b3d573f2a10aa2eff2c01a8a386dd304e7fda"
echo "grass.glb 8e11283ca11894860a4dcb8f523165eda5de39f68fda43d892efc3a3d3eb567e"
echo "platform-large-grass.glb 19ee4b9e745f757d87d584094433e3f4d86be958bcd7eeba586874f088d2697b"
echo "platform.glb 6eac72ffbda64b4414833eb49d3b160fddf169c2caccca237a07e15a20731764"
echo "wall-high.glb 946a0f875bd515323afd883553729a29ac764b405e382bf9a30d6527dac74589"
echo "wall-low.glb c42c89dead3835fde97d1ede232bf1a69d297cd41e1d6bfe7e25c87b233a2ec0"
;;
platformer)
echo "block-coin.glb ebe3a7be051566513039e6ba7d83d976fb3bf3e363fc8ee0633bdb872bb2d501"
echo "brick-particle.glb 981a84073e1dfa745a93db4ba550210049a144a4ac2c6cd333f289ff95630a72"
echo "brick.glb c9011dd30254c7a5dfdb29c61c22b6b3b4ad82186a096b90b824ab0b8dd58db8"
echo "character.glb 7112f6a08400914f9da546f3e6029e947cc9eab2b4a6da5eb99776111289efb1"
echo "cloud.glb 09c6f071d3a9ab64993248ee5c8d50debcd9c6cc4369236a01155ff7ff87f30a"
echo "coin.glb a3fb8f779a6af1cd75e5a02da77a87546ca8a59f6fe52f79f25179c4a68fd0e5"
echo "dust.glb 2f2ce449aca7791829bf2d93a7af534549f0bad1ee904d1a00df8d5329fc31dc"
echo "flag.glb 0ba84ff43f0fea0a9f0b080cfa1be54ebda36c8c35174d1ac2553ae6da9f65ae"
echo "grass-small.glb 4b94e8dd9cbdc3eedff99cbf614fe828f0c2342d21a7d0edb125b129d0422d51"
echo "grass.glb acb29df8a75b08e985e27a0ed8170b8f3d135abd24ccf5aa4d631cb2cd15cfda"
echo "platform-falling.glb d6dc9baba80af659e6d0fffc435bd1f08b1a7945942ccbb85dd9bb103b7a574d"
echo "platform-grass-large-round.glb 73c2b66eca6a36df6b5c25bb2c0594cf870066627275993a3a7857acc5a844bc"
echo "platform-large.glb d01f4eae24c895dfe467ef71c2ea510ca6bd6d8462f319741537936eae0b3f65"
echo "platform-medium.glb 64f81c8bd8bf07cc450dfcb25d18eedcc122d690028cca098bcdd48b00a02e46"
echo "platform.glb 78c5ed4da30c5a97f0747a54248bc9de05ba1b3fb6b94be527dd48599f5ef44c"
;;
racing)
echo "decoration-empty.glb 3815b26a5274173934d37cf605e320450efad4e0034040868b6aea761cfd74bc"
echo "decoration-forest.glb 664a53f0f709fef9096af3bbfb1aa76536527a616b05170c0cf4e27e33358a00"
echo "decoration-tents.glb 19dbf2a778ad75f95c7d61f12866ef7174ab69cad169d3749c5400f5e14db8a3"
echo "track-bump.glb 6db020edc53532ebc0971c9d4ad9afb11c4de67c510649ca843db4616514f43c"
echo "track-corner.glb 0ffb3d83b60456fc5a2962447111a1a64bfe943f4ad95462e1edba107be81c80"
echo "track-finish.glb 2fec3b681658d6e77e20c4342d1e3ceeaab3e8d2fbf340942cde25cc2b21975b"
echo "track-straight.glb 2d8080df1fe27e39981480809f36eba7f239e814f96bff61bb4c99793221b701"
echo "track-tents.glb eaf68bbb44e362e291b71d13015fbafc4ce22a9102bda029614972d6cc843c7f"
echo "vehicle-motorcycle.glb c97911b8dbc2d5dd9d1961b46eb4d1f132b67a6c5a84f45593fea44e0595dd92"
echo "vehicle-truck-green.glb df362d027a09b19395be0e96abd7cc2dc54608dfb920b592e64357b589f9fcd6"
echo "vehicle-truck-purple.glb 03e32f5abcbbd03da3591f42fdaae2cf7d27ed379df7ce0d1f233bea02998565"
echo "vehicle-truck-red.glb eca99bd9ab0a2b02125f915e65d1ec8f1c5a93be7c6d4840efdac6633f47772c"
echo "vehicle-truck-yellow.glb 1ebd83174eab6d2fdf69eb5d8e32bd06a23a86d9edc4fe915a4160430a7c36a5"
;;
*)
echo "unknown pack: $1" >&2
return 1
;;
esac
}
pack_count() { pack_files "$1" | wc -l | tr -d ' '; }
# Read MIRROR.toml into parallel arrays: slug, url, sha, models, core, usable.
manifest_rows() {
awk '
/^\[\[pack\]\]/ { slug=url=sha=""; models=0; core="false"; usable="false"; next }
/^slug =/ { gsub(/.*= "|"/, ""); slug=$0; next }
/^url =/ { gsub(/.*= "|"/, ""); url=$0; next }
/^sha256 =/ { gsub(/.*= "|"/, ""); sha=$0; next }
/^models =/ { gsub(/[^0-9]/, ""); models=$0; next }
/^core =/ { gsub(/.*= /, ""); core=$0; next }
/^usable =/ { gsub(/.*= /, ""); usable=$0
if (slug != "") print slug "|" url "|" sha "|" models "|" core "|" usable
next }
' "$MANIFEST"
}
want_pack() { # <slug> <core>
case "$PACKSEL" in
all) return 0 ;;
core) [[ "$2" == "true" ]] ;;
*) [[ ",$PACKSEL," == *",$1,"* ]] ;;
esac
}
if [[ "${1:-}" == "--list" ]]; then
echo "Makepad Arcade stock asset library"
echo
printf '%-12s %-6s %-10s %s\n' "PACK" "FILES" "LICENCE" "SOURCE"
for entry in "${KENNEY_PACKS[@]}"; do
IFS='|' read -r pack repo _commit _dir <<<"$entry"
printf '%-12s %-6s %-10s %s\n' "$pack" "$(pack_count "$pack")" "CC0-1.0" "kenney.nl (KenneyNL/$repo)"
done
printf '%-12s %-6s %-10s %s\n' "characters" "${#KAYKIT_CHARS[@]}" "CC0-1.0" \
"kaylousberg.com (KayKit Adventurers + Skeletons, one shared rig)"
for entry in "${KENNEY_AUDIO[@]}"; do
IFS='|' read -r pack _hash _sha count <<<"$entry"
printf '%-12s %-6s %-10s %s\n' "$pack" "$count" "CC0-1.0" "kenney.nl (ogg)"
done
if [[ -f "$MANIFEST" ]]; then
echo
echo "3D packs (from MIRROR.toml; * = in the default core set):"
tm=0; tp=0
while IFS='|' read -r slug _url _sha models core usable; do
[[ -z "$slug" ]] && continue
mark=" "; [[ "$core" == "true" ]] && mark="*"
note=""; [[ "$usable" == "true" ]] || note=" (FBX only — not fetched)"
printf '%s %-34s %5s models%s\n' "$mark" "$slug" "$models" "$note"
[[ "$usable" == "true" ]] || continue
tm=$((tm + models)); tp=$((tp + 1))
done < <(manifest_rows)
echo " ${tp} usable packs, ${tm} models total"
fi
echo
echo "Run without --list to download. Files land in resources/models/kenney/<pack>/,"
echo "resources/audio/kenney/<pack>/ and resources/characters/ — all gitignored."
echo
echo "Audio is Ogg Vorbis (Kenney ships no WAV). This tree has no vorbis decoder,"
echo "so sounds index and search but do not play yet; --transcode converts them to"
echo "WAV with ffmpeg if you have it installed."
exit 0
fi
TRANSCODE=0
for arg in "$@"; do
case "$arg" in
--transcode) TRANSCODE=1 ;;
--packs=*) PACKSEL="${arg#--packs=}" ;;
--mirror=*) MIRROR="${arg#--mirror=}" ;;
--list) : ;;
*) echo "unknown option: $arg" >&2; exit 1 ;;
esac
done
fetch() { # <url> <dest> <sha256> <label>
local url="$1" dest="$2" sha="$3" label="$4"
if [[ -f "$dest" ]] && echo "$sha $dest" | shasum -a 256 -c - >/dev/null 2>&1; then
cached=$((cached + 1))
return 0
fi
if ! curl -sSfL "$url" -o "$dest.tmp"; then
echo "ERROR: download failed: $label" >&2
echo " $url" >&2
rm -f "$dest.tmp"
exit 1
fi
if ! echo "$sha $dest.tmp" | shasum -a 256 -c - >/dev/null 2>&1; then
echo "ERROR: sha256 mismatch for $label (upstream changed or download corrupted)" >&2
echo " expected: $sha" >&2
echo " got: $(shasum -a 256 "$dest.tmp" | awk '{print $1}')" >&2
rm -f "$dest.tmp"
exit 1
fi
mv "$dest.tmp" "$dest"
fetched=$((fetched + 1))
}
# URL-encode spaces only; upstream paths use no other unsafe characters.
urlenc() { printf '%s' "$1" | sed 's/ /%20/g'; }
for entry in "${KENNEY_PACKS[@]}"; do
IFS='|' read -r pack repo commit dir <<<"$entry"
mkdir -p "$MODELS/$pack"
n=$(pack_count "$pack")
echo "kenney/$pack ($n models)"
while read -r name sha; do
[[ -z "$name" ]] && continue
url="https://raw.githubusercontent.com/KenneyNL/$repo/$commit/$(urlenc "$dir/$name")"
fetch "$url" "$MODELS/$pack/$name" "$sha" "kenney/$pack/$name"
done < <(pack_files "$pack")
done
mkdir -p "$CHARS"
echo "kaykit/characters (${#KAYKIT_CHARS[@]} rigged models, one shared 41-joint rig)"
for entry in "${KAYKIT_CHARS[@]}"; do
IFS='|' read -r local pack file sha _tex <<<"$entry"
fetch "$(kaykit_base "$pack")/$file" "$CHARS/$local.glb" "$sha" "kaykit/$local.glb"
done
for entry in "${KAYKIT_TEXTURES[@]}"; do
IFS='|' read -r local pack file sha <<<"$entry"
fetch "$(kaykit_base "$pack")/$file" "$CHARS/$local" "$sha" "kaykit/$local"
done
# ---- 3D model packs (manifest-driven) -------------------------------------
if [[ -f "$MANIFEST" ]]; then
while IFS='|' read -r slug url sha models core usable; do
[[ -z "$slug" ]] && continue
[[ "$usable" == "true" ]] || continue
want_pack "$slug" "$core" || continue
dest="$MODELS/$slug"
# The marker is written only after a COMPLETE extraction (models plus
# any texture atlas). Counting models alone declared a pack cached when
# its colormap.png had never been extracted at all — 48 of 52 packs
# rendered untextured while the script cheerfully reported them cached.
if [[ -f "$dest/.extracted" ]] && [[ $(find "$dest" -iname '*.glb' -o -iname '*.gltf' | wc -l | tr -d ' ') -ge "$models" ]]; then
cached=$((cached + 1))
continue
fi
# A mirror serves <base>/<slug>.zip; upstream keeps its own layout.
src="$url"
[[ -n "$MIRROR" ]] && src="$MIRROR/$slug.zip"
echo "kenney/$slug ($models models)"
zip="$MODELS/.$slug.zip"
mkdir -p "$MODELS"
fetch "$src" "$zip" "$sha" "models/$slug"
mkdir -p "$dest"
unzip -qo "$zip" -d "$zip.d" 2>/dev/null
chmod -R u+w "$zip.d" 2>/dev/null
# Prefer GLB and skip the OBJ/FBX/DAE copies — they are most of the
# archive and we cannot load them. NOTE GLB is NOT self-contained here:
# Kenney materials reference an external `Textures/colormap.png` shared
# by the whole pack, and that URI is RELATIVE TO THE GLB — so the
# directory structure has to survive extraction. Flattening the PNGs
# into the pack root leaves every model looking for a path that no
# longer exists, which is indistinguishable from having no texture.
# Only the atlas is wanted: the archives also carry Preview/Sample and
# per-model thumbnails, which are ~200 MB of images nothing loads.
# The GLB's URI is `Textures/colormap.png` relative to itself, and GLBs
# land in the pack root — so keep the `Textures/` tail and drop
# everything above it (archives nest it under e.g. `FBX format/`,
# which varies per pack and must not survive).
while IFS= read -r tex; do
rel="Textures/${tex##*/Textures/}"
mkdir -p "$dest/$(dirname "$rel")"
mv -f "$tex" "$dest/$rel"
done < <(find "$zip.d" -ipath '*/Textures/*.png' ! -iname 'Preview*' ! -iname 'Sample*')
if [[ $(find "$zip.d" -iname '*.glb' | wc -l | tr -d ' ') -gt 0 ]]; then
find "$zip.d" -iname '*.glb' -exec sh -c 'mv -f "$1" "$2/$(basename "$1")"' _ {} "$dest" \;
else
find "$zip.d" \( -iname '*.gltf' -o -iname '*.bin' \) -exec sh -c 'mv -f "$1" "$2/$(basename "$1")"' _ {} "$dest" \;
fi
rm -rf "$zip.d" "$zip"
: >"$dest/.extracted"
# Be a good guest: pace requests so a full run is a trickle, not a flood.
sleep 1
done < <(manifest_rows)
fi
for entry in "${KENNEY_AUDIO[@]}"; do
IFS='|' read -r pack hash sha count <<<"$entry"
dest="$AUDIO/$pack"
zip="$AUDIO/.$pack.zip"
if [[ -d "$dest" ]] && [[ $(find "$dest" -name '*.ogg' | wc -l | tr -d ' ') -ge "$count" ]]; then
cached=$((cached + 1))
continue
fi
echo "kenney/$pack ($count sounds)"
mkdir -p "$AUDIO"
fetch "https://kenney.nl/media/pages/assets/$pack/$hash/kenney_$pack.zip" "$zip" "$sha" "audio/$pack"
mkdir -p "$dest"
# Flatten: the packs nest under Audio/ or Sounds/, and we only want the
# sound files — never the bundled .url/.txt/preview cruft.
unzip -qo "$zip" -d "$zip.d"
# Some packs ship directories without the owner write bit (sci-fi-sounds
# is mode r-xr-xr-x), which blocks both the move out and the cleanup.
chmod -R u+w "$zip.d"
find "$zip.d" -name '*.ogg' -exec mv -f {} "$dest/" \;
rm -rf "$zip.d" "$zip"
done
if [[ $TRANSCODE == 1 ]]; then
if command -v ffmpeg >/dev/null 2>&1; then
echo "transcoding ogg -> wav (ffmpeg)"
find "$AUDIO" -name '*.ogg' | while read -r f; do
w="${f%.ogg}.wav"
[[ -f "$w" ]] || ffmpeg -loglevel error -y -i "$f" "$w"
done
else
echo "WARNING: --transcode needs ffmpeg on PATH; skipping" >&2
fi
fi
echo
echo "done — $fetched fetched, $cached already cached"
echo
echo "These assets are CC0 (public domain). Attribution isn't required, but is"
echo "deserved:"
echo " Kenney https://kenney.nl/assets"
echo " KayKit / Kay Lousberg https://kaylousberg.itch.io/"
echo
echo "Thank you both for giving so many high-quality assets away for free."