#!/bin/bash # # Refresh the tutorial art in docs/tutorials/assets/ from Kenney.nl. # # Those PNGs are tracked on purpose. They are what the two tutorial games in # examples/ draw, what docs/tutorials/PROVENANCE.md accounts for, and what a # reader copies into their own project -- so the tree has to build and the # tutorials have to run with no network access at all. This script therefore # has the same rule mkcontrollermappings.sh was fixed into for 0.5.0: **it must # never replace a good tracked asset with a worse one**. Everything is fetched # and repacked in a temporary directory, every step is checked, and the staged # files are moved into place only once all of them have come out right. Any # failure leaves the tracked copies exactly as they were and exits non-zero. # # Nothing in the build runs this. It is a deliberate, standalone refresh, and # its output should land in its own commit so the diff is reviewable. # # Two of the eight images are verbatim copies of a Kenney tileset. The other six # are repacked: 16x16 source art composited bottom-centre into 32x32 cells, one # row, left to right, which is the layout akgl_spritesheet_coords_for_frame # indexes. The repack lives here rather than in a second script so that the # tracked bytes are reproducible from one command. # # The JSON (sprites, characters) and the TMJ maps are hand-authored source, not # generated. This script does not touch them. # # Usage: scripts/fetch_tutorial_assets.sh [repository-root] set -euo pipefail AKGL_KENNEY_ASSET_URL="${AKGL_KENNEY_ASSET_URL:-https://kenney.nl/assets}" # A floor, not a target. Each pack page carries its own licence line; a page # that does not say CC0 is either the wrong page or a relicensed pack, and # either way its bytes must not land in this repository. AKGL_KENNEY_LICENSE_TEXT="${AKGL_KENNEY_LICENSE_TEXT:-Creative Commons CC0}" function die() { echo "fetch_tutorial_assets: $*" >&2 exit 1 } function note() { echo "fetch_tutorial_assets: $*" } rootdir="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" assetdir="${rootdir}/docs/tutorials/assets" if [[ ! -d "${assetdir}/sidescroller" ]] || [[ ! -d "${assetdir}/jrpg" ]]; then die "no such directory: ${assetdir}/{sidescroller,jrpg}" fi command -v curl >/dev/null 2>&1 || die "curl is not available" command -v unzip >/dev/null 2>&1 || die "unzip is not available" command -v convert >/dev/null 2>&1 || die "ImageMagick 'convert' is not available" command -v identify >/dev/null 2>&1 || die "ImageMagick 'identify' is not available" workdir="$(mktemp -d)" function cleanup() { rm -rf "${workdir}" } trap cleanup EXIT staging="${workdir}/staging" mkdir -p "${staging}/sidescroller" "${staging}/jrpg" ## ## Fetching ## # Download one pack, check its page says CC0, and unpack it under ${workdir}/. function fetch_pack() { local slug="$1" local page="${workdir}/${slug}.html" local zip="${workdir}/${slug}.zip" local dest="${workdir}/${slug}" local url="" # --fail so an HTTP error status is an exit status rather than an error page # in the body. Not a pipeline: the status being checked is curl's, and a # pipeline reports the last command's. if ! curl --fail --silent --show-error --location "${AKGL_KENNEY_ASSET_URL}/${slug}" --output "${page}"; then die "could not fetch the pack page for ${slug}; tracked assets left as they were" fi if ! grep -q "${AKGL_KENNEY_LICENSE_TEXT}" "${page}"; then die "the ${slug} page does not say '${AKGL_KENNEY_LICENSE_TEXT}'; refusing to vendor it" fi url="$(grep -o "https://[^'\"]*${slug}\.zip" "${page}" | head -1)" if [[ -z "${url}" ]]; then die "no download link found on the ${slug} page; tracked assets left as they were" fi if ! curl --fail --silent --show-error --location "${url}" --output "${zip}"; then die "download failed from ${url}; tracked assets left as they were" fi if ! unzip -tq "${zip}" >/dev/null 2>&1; then die "${slug}.zip did not survive an integrity check; tracked assets left as they were" fi mkdir -p "${dest}" if ! unzip -qo "${zip}" -d "${dest}"; then die "could not unpack ${slug}.zip; tracked assets left as they were" fi if ! grep -q "Creative Commons Zero" "${dest}/License.txt"; then die "${slug} does not carry a CC0 License.txt; refusing to vendor it" fi note "fetched ${slug} from ${url}" } # Refuse to publish an audio file that is not actually Ogg. `file(1)` is not # guaranteed to be installed; the container magic is four bytes at offset zero. function require_ogg() { local file="$1" local minbytes="$2" [[ -f "${file}" ]] || die "expected file is missing: ${file}" if [[ "$(head -c 4 "${file}")" != "OggS" ]]; then die "${file} is not an Ogg stream; tracked assets left as they were" fi if [[ "$(wc -c < "${file}")" -lt "${minbytes}" ]]; then die "${file} is under ${minbytes} bytes; tracked assets left as they were" fi } # Refuse to work from an image that is not the size the frame arithmetic assumes. function require_size() { local file="$1" local want="$2" local got="" [[ -f "${file}" ]] || die "expected file is missing: ${file}" got="$(identify -format '%wx%h' "${file}")" if [[ "${got}" != "${want}" ]]; then die "${file} is ${got}, expected ${want}; the pack layout has changed and the frame offsets no longer hold" fi } ## ## Repacking ## ## A sheet is one row of 32x32 cells. Each cell holds one 16x16 source tile ## composited at (8, 16) -- horizontally centred, sitting on the cell's bottom ## edge -- so an actor drawn into a 32x32 destination rectangle has its feet on ## the bottom of that rectangle whichever sheet it came from. ## AKGL_SHEET_CELL=32 AKGL_SOURCE_TILE=16 AKGL_CELL_OFFSET_X=8 AKGL_CELL_OFFSET_Y=16 # build_sheet ... # # Each spec is "X,Y" or "X,Y,flop": the source pixel offset of a 16x16 tile, and # whether to mirror it horizontally. Specs are laid into cells left to right, so # the position of a spec in the argument list *is* its frame id. function build_sheet() { local src="$1" local out="$2" shift 2 local -a args=() local count=$# local idx=0 local spec="" local sx="" local sy="" local flop="" args+=( -size "$((count * AKGL_SHEET_CELL))x${AKGL_SHEET_CELL}" xc:none ) for spec in "$@"; do IFS=',' read -r sx sy flop <<< "${spec}" args+=( '(' "${src}" -crop "${AKGL_SOURCE_TILE}x${AKGL_SOURCE_TILE}+${sx}+${sy}" +repage ) if [[ "${flop:-}" == "flop" ]]; then args+=( -flop ) fi args+=( ')' -geometry "+$(( (idx * AKGL_SHEET_CELL) + AKGL_CELL_OFFSET_X ))+${AKGL_CELL_OFFSET_Y}" -composite ) idx=$((idx + 1)) done # -strip, and the two date properties by name, because ImageMagick writes # date:create/date:modify tEXt chunks from the wall clock. Without this a # refresh that changed nothing at all still produced eight different files, # and the diff would say the art had changed when it had not. args+=( -strip +set date:create +set date:modify +set date:timestamp ) if ! convert "${args[@]}" "${out}"; then die "could not repack ${out}; tracked assets left as they were" fi } # rpg_urban_character # # The RPG Urban Pack lays each character out as a 4-column by 3-row block in the # last four columns of the tileset: columns are left, down, up, right; rows are # stand, step A, step B. This regroups one block into the twelve-frame order the # tutorial sprite JSON expects -- down, left, right, up, three frames each. function rpg_urban_character() { local charidx="$1" local out="$2" local src="${workdir}/rpg-urban-pack/Tilemap/tilemap_packed.png" local left=368 local down=384 local up=400 local right=416 local y0=$(( charidx * 3 * 16 )) local y1=$(( y0 + 16 )) local y2=$(( y0 + 32 )) build_sheet "${src}" "${out}" \ "${down},${y0}" "${down},${y1}" "${down},${y2}" \ "${left},${y0}" "${left},${y1}" "${left},${y2}" \ "${right},${y0}" "${right},${y1}" "${right},${y2}" \ "${up},${y0}" "${up},${y1}" "${up},${y2}" } ## ## Do the work ## fetch_pack "pixel-line-platformer" fetch_pack "rpg-urban-pack" fetch_pack "music-jingles" plp="${workdir}/pixel-line-platformer/Tilemap/tilemap_packed.png" rup="${workdir}/rpg-urban-pack/Tilemap/tilemap_packed.png" require_size "${plp}" "160x96" require_size "${rup}" "432x288" # Tilesets are copied verbatim. Both are already 16x16 with zero spacing and # zero margin, which is the only geometry akgl_tilemap_compute_tileset_offsets # gets right: it adds `spacing` to the tile pitch but sets the first row's y # offset to `spacing` rather than zero, and it ignores `margin` entirely. cp "${plp}" "${staging}/sidescroller/tiles.png" cp "${rup}" "${staging}/jrpg/tiles.png" # Pixel Line Platformer tile ids, ten columns: id -> ((id % 10) * 16, (id / 10) * 16) # 40, 41, 42 the armed rabbit: contact, passing (also the jump pose), contact # 44 the gold pickup # 51, 52 the moth, wings up and down # 55, 56 the red blob, two frames # # akgl_actor_render always draws with SDL_FLIP_NONE, so a left-facing run has to # exist in the sheet as its own frames rather than being mirrored at draw time. build_sheet "${plp}" "${staging}/sidescroller/player.png" \ "0,64" "16,64" "32,64" \ "0,64,flop" "16,64,flop" "32,64,flop" build_sheet "${plp}" "${staging}/sidescroller/coin.png" \ "64,64" build_sheet "${plp}" "${staging}/sidescroller/hazard.png" \ "80,80" "96,80" "16,80" "32,80" rpg_urban_character 0 "${staging}/jrpg/player.png" rpg_urban_character 3 "${staging}/jrpg/npc_shopkeeper.png" rpg_urban_character 2 "${staging}/jrpg/npc_elder.png" # One jingle per game, copied unchanged. akgl_load_start_bgm() is the library's # only file-audio entry point -- akgl_audio_* is a synthesiser, and there is no # sound-effect loader at all -- so a jingle is what a tutorial can actually # play. Both are stings of under two seconds, and the loop request in # akgl_load_start_bgm does not take effect, so each plays once. cp "${workdir}/music-jingles/Audio/8-Bit jingles/jingles_NES00.ogg" \ "${staging}/sidescroller/jingle_start.ogg" cp "${workdir}/music-jingles/Audio/Pizzicato jingles/jingles_PIZZI07.ogg" \ "${staging}/jrpg/jingle_start.ogg" cp "${workdir}/pixel-line-platformer/License.txt" "${staging}/LICENSE.kenney_pixel-line-platformer.txt" cp "${workdir}/rpg-urban-pack/License.txt" "${staging}/LICENSE.kenney_rpg-urban-pack.txt" cp "${workdir}/music-jingles/License.txt" "${staging}/LICENSE.kenney_music-jingles.txt" ## ## Check the staging area before anything is allowed near the tracked copies ## # name expected size AKGL_TUTORIAL_ASSETS=( "sidescroller/tiles.png 160x96" "sidescroller/player.png 192x32" "sidescroller/coin.png 32x32" "sidescroller/hazard.png 128x32" "jrpg/tiles.png 432x288" "jrpg/player.png 384x32" "jrpg/npc_shopkeeper.png 384x32" "jrpg/npc_elder.png 384x32" ) staged=0 for entry in "${AKGL_TUTORIAL_ASSETS[@]}"; do read -r name want <<< "${entry}" require_size "${staging}/${name}" "${want}" if [[ ! -s "${staging}/${name}" ]]; then die "staged ${name} is empty; tracked assets left as they were" fi staged=$((staged + 1)) done if [[ "${staged}" -ne "${#AKGL_TUTORIAL_ASSETS[@]}" ]]; then die "staged ${staged} images, expected ${#AKGL_TUTORIAL_ASSETS[@]}; tracked assets left as they were" fi AKGL_TUTORIAL_AUDIO=( "sidescroller/jingle_start.ogg" "jrpg/jingle_start.ogg" ) for name in "${AKGL_TUTORIAL_AUDIO[@]}"; do require_ogg "${staging}/${name}" 4096 done AKGL_TUTORIAL_LICENSES=( "LICENSE.kenney_pixel-line-platformer.txt" "LICENSE.kenney_rpg-urban-pack.txt" "LICENSE.kenney_music-jingles.txt" ) for name in "${AKGL_TUTORIAL_LICENSES[@]}"; do if [[ ! -s "${staging}/${name}" ]]; then die "staged ${name} is empty; tracked assets left as they were" fi done ## ## Publish ## for entry in "${AKGL_TUTORIAL_ASSETS[@]}"; do read -r name want <<< "${entry}" mv "${staging}/${name}" "${assetdir}/${name}" done for name in "${AKGL_TUTORIAL_AUDIO[@]}" "${AKGL_TUTORIAL_LICENSES[@]}"; do mv "${staging}/${name}" "${assetdir}/${name}" done note "wrote ${staged} images, ${#AKGL_TUTORIAL_AUDIO[@]} jingles and ${#AKGL_TUTORIAL_LICENSES[@]} upstream licence files to ${assetdir}"