├── utils ├── mkasm.py ├── mkhtml.py ├── jswrelease ├── build-skool ├── skrunner.py ├── write-tests.py ├── build-disassembly ├── jswimage.py └── jsw2skool.py ├── Makefile ├── .gitignore ├── sources ├── dark.css ├── plum.css ├── green.css ├── jsw.css ├── COPYING ├── sound.ref ├── glossary.ref ├── jetsetwilly.py ├── pokes.ref ├── changelog.ref ├── bugs.ref ├── facts.ref └── jsw.ref ├── .ddiffsrc ├── .dreleaserc ├── jet_set_willy.t2s ├── .github └── workflows │ └── tests.yml └── README.md /utils/mkasm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from skrunner import run_skool2asm 3 | 4 | run_skool2asm() 5 | -------------------------------------------------------------------------------- /utils/mkhtml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from skrunner import run_skool2html 3 | 4 | run_skool2html() 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MK = $(SKOOLKIT_HOME)/tools/disassembly.mk 2 | ifeq ($(wildcard $(MK)),) 3 | $(error $(MK): file not found) 4 | endif 5 | include $(MK) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .clinesignore 3 | .emacs.desktop 4 | .emacs.desktop.lock 5 | build 6 | diffs 7 | dist 8 | tests/test_asm.py 9 | tests/test_ctl.py 10 | tests/test_html.py 11 | untracked 12 | -------------------------------------------------------------------------------- /sources/dark.css: -------------------------------------------------------------------------------- 1 | table.gallery td { 2 | border-color: #ffffff; 3 | } 4 | table.boxData td { 5 | background-color: #664422; 6 | } 7 | table.boxData th { 8 | background-color: #886644; 9 | } 10 | -------------------------------------------------------------------------------- /sources/plum.css: -------------------------------------------------------------------------------- 1 | table.gallery td { 2 | border-color: #ffffff; 3 | } 4 | table.boxData td { 5 | background-color: #993366; 6 | } 7 | table.boxData th { 8 | background-color: #cc6699; 9 | } 10 | -------------------------------------------------------------------------------- /sources/green.css: -------------------------------------------------------------------------------- 1 | table.gallery td { 2 | border-color: #ffffff; 3 | } 4 | table.boxData td { 5 | background-color: #669933; 6 | } 7 | table.boxData th { 8 | background-color: #99cc66; 9 | } 10 | -------------------------------------------------------------------------------- /.ddiffsrc: -------------------------------------------------------------------------------- 1 | # rc file for check-asms, check-bins, disassembly-diff, skooldiff, write-files 2 | 3 | OLD_VERSION=20221122 4 | SK_OLD_VERSION=8.8 5 | SKOOL2BIN_START=32765 6 | SNAPSHOT=$JETSETWILLY_HOME/build/jet_set_willy.z80 7 | -------------------------------------------------------------------------------- /.dreleaserc: -------------------------------------------------------------------------------- 1 | GAME=jsw 2 | SRCFILES=$(echo jsw.skool {jsw,bugs,changelog,facts,glossary,pokes,sound}.ref {jsw,dark,green,plum}.css jetsetwilly.py COPYING) 3 | SKOOL2BINOPTS="-i -S 32765" 4 | WAVCONVERT="falling.wav game-over.wav in-game-tune.wav jumping.wav screech.wav tune.wav" 5 | -------------------------------------------------------------------------------- /jet_set_willy.t2s: -------------------------------------------------------------------------------- 1 | ; tap2sna.py file for Jet Set Willy. Run: 2 | ; 3 | ; $ tap2sna.py @jet_set_willy.t2s 4 | ; 5 | ; to create a Z80 snapshot. 6 | 7 | https://worldofspectrum.org/pub/sinclair/games/j/JetSetWilly(original).tap.zip 8 | --ram poke=23672,248 # Ask for the code at L1 (1234) 9 | -------------------------------------------------------------------------------- /sources/jsw.css: -------------------------------------------------------------------------------- 1 | table.gallery { 2 | border-collapse: collapse; 3 | } 4 | table.gallery td { 5 | text-align: center; 6 | border-width: 1px; 7 | border-style: solid; 8 | border-color: #000000; 9 | padding: 5px; 10 | } 11 | table.gallery td.transparent { 12 | border-style: none; 13 | } 14 | table.boxData td { 15 | background-color: #99bbdd; 16 | } 17 | table.boxData th { 18 | background-color: #7799bb; 19 | } 20 | -------------------------------------------------------------------------------- /sources/COPYING: -------------------------------------------------------------------------------- 1 | The SkoolKit extension module (jetsetwilly.py) is licensed under the GNU 2 | General Public License, version 3. To view a copy of this licence, visit 3 | 4 | http://www.gnu.org/licenses/ 5 | 6 | The text of the disassembly itself (annotations, the contents of the Trivia, 7 | Bugs and Pokes pages etc.) is licensed under the Creative Commons 8 | Attribution-ShareAlike 3.0 Unported License. To view a copy of this licence, 9 | visit 10 | 11 | http://creativecommons.org/licenses/by-sa/3.0/ 12 | -------------------------------------------------------------------------------- /utils/jswrelease: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e # Abort on errors 3 | 4 | if [[ -z $JETSETWILLY_HOME ]]; then 5 | echo "Error: JETSETWILLY_HOME not defined." 6 | exit 1 7 | fi 8 | if [[ ! -d $JETSETWILLY_HOME ]]; then 9 | echo "Error: directory not found: $JETSETWILLY_HOME" 10 | exit 1 11 | fi 12 | 13 | if [[ -z $SKOOLKIT_HOME ]]; then 14 | echo "Error: SKOOLKIT_HOME not defined." 15 | exit 1 16 | fi 17 | if [[ ! -d $SKOOLKIT_HOME ]]; then 18 | echo "Error: directory not found: $SKOOLKIT_HOME" 19 | exit 1 20 | fi 21 | 22 | DHOME=$JETSETWILLY_HOME 23 | 24 | . $DHOME/.dreleaserc 25 | 26 | . $SKOOLKIT_HOME/tools/drelease-functions 27 | 28 | parse_args $* 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install nose2 & lxml 25 | run: | 26 | python -m pip install --upgrade pip 27 | python -m pip install nose2 lxml 28 | 29 | - name: Install SkoolKit 30 | run: | 31 | wget https://github.com/skoolkid/skoolkit/archive/refs/heads/master.zip 32 | unzip master.zip 33 | 34 | - name: Test with nose2 35 | run: | 36 | export SKOOLKIT_HOME=$(pwd)/skoolkit-master 37 | make write-tests 38 | nose2 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jet Set Willy disassembly 2 | ========================= 3 | 4 | A disassembly of the [Spectrum](https://en.wikipedia.org/wiki/ZX_Spectrum) 5 | version of [Jet Set Willy](https://en.wikipedia.org/wiki/Jet_Set_Willy), 6 | created using [SkoolKit](https://skoolkit.ca/). 7 | 8 | Decide which number base you prefer and then click the corresponding link below 9 | to browse the latest release: 10 | 11 | * [Jet Set Willy disassembly](https://skoolkid.github.io/jetsetwilly/) (hexadecimal; mirror [here](https://skoolkid.gitlab.io/jetsetwilly/)) 12 | * [Jet Set Willy disassembly](https://skoolkid.github.io/jetsetwilly/dec/) (decimal; mirror [here](https://skoolkid.gitlab.io/jetsetwilly/dec/)) 13 | 14 | To build the current development version of the disassembly, first obtain the 15 | development version of [SkoolKit](https://github.com/skoolkid/skoolkit). Then: 16 | 17 | $ skool2html.py sources/jsw.skool 18 | 19 | To build an assembly language source file that can be fed to an assembler: 20 | 21 | $ skool2asm.py sources/jsw.skool > jsw.asm 22 | -------------------------------------------------------------------------------- /utils/build-skool: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if (($# > 0)); then 4 | cat >&2 << EOM 5 | Usage: $(basename $0) 6 | 7 | Build a skool file using the existing contents of jsw.skool and the snippets 8 | produced by jsw2skool.py. 9 | EOM 10 | exit 1 11 | fi 12 | 13 | if [[ -z $JETSETWILLY_HOME ]]; then 14 | echo "Error: JETSETWILLY_HOME not defined." 15 | exit 1 16 | fi 17 | if [[ ! -d $JETSETWILLY_HOME ]]; then 18 | echo "Error: directory not found: $JETSETWILLY_HOME" 19 | exit 1 20 | fi 21 | 22 | jsw2skool=$JETSETWILLY_HOME/utils/jsw2skool.py 23 | JSW_SKOOL=$JETSETWILLY_HOME/sources/jsw.skool 24 | 25 | sed '/^s33089 .*/q' $JSW_SKOOL 26 | echo 27 | $jsw2skool sbat 28 | sed -n '/^; Rope animation table/,/^ 40704 .*/p' $JSW_SKOOL 29 | echo 30 | $jsw2skool entity-defs 31 | sed -n '/^; Index of the first item/,/^b41983 DEFB 173/p' $JSW_SKOOL 32 | echo 33 | $jsw2skool items 34 | sed -n '/^; Toilet graphics/,/^ 42752 DEFS 1024/p' $JSW_SKOOL 35 | echo 36 | $jsw2skool guardians 37 | $jsw2skool rooms 38 | sed -n '/; Unused TRS-DOS code/,//p' $JSW_SKOOL 39 | -------------------------------------------------------------------------------- /utils/skrunner.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | JETSETWILLY_HOME = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | JSW_SKOOL = '{}/sources/jsw.skool'.format(JETSETWILLY_HOME) 6 | 7 | SKOOLKIT_HOME = os.environ.get('SKOOLKIT_HOME') 8 | if SKOOLKIT_HOME: 9 | if not os.path.isdir(SKOOLKIT_HOME): 10 | sys.stderr.write('SKOOLKIT_HOME={}: directory not found\n'.format(SKOOLKIT_HOME)) 11 | sys.exit(1) 12 | sys.path.insert(0, SKOOLKIT_HOME) 13 | from skoolkit import skool2asm, skool2html, sna2skool, tap2sna 14 | else: 15 | try: 16 | from skoolkit import skool2asm, skool2html, sna2skool, tap2sna 17 | except ImportError: 18 | sys.stderr.write('Error: SKOOLKIT_HOME is not set, and SkoolKit is not installed\n') 19 | sys.exit(1) 20 | 21 | sys.stderr.write("Found SkoolKit in {}\n".format(skool2html.PACKAGE_DIR)) 22 | 23 | def run_skool2asm(): 24 | skool2asm.main(sys.argv[1:] + [JSW_SKOOL]) 25 | 26 | def run_skool2html(): 27 | options = '-d {}/build/html'.format(JETSETWILLY_HOME) 28 | skool2html.main(options.split() + sys.argv[1:] + [JSW_SKOOL]) 29 | -------------------------------------------------------------------------------- /utils/write-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | SKOOLKIT_HOME = os.environ.get('SKOOLKIT_HOME') 6 | if not SKOOLKIT_HOME: 7 | sys.stderr.write('SKOOLKIT_HOME is not set; aborting\n') 8 | sys.exit(1) 9 | if not os.path.isdir(SKOOLKIT_HOME): 10 | sys.stderr.write('SKOOLKIT_HOME={}: directory not found\n'.format(SKOOLKIT_HOME)) 11 | sys.exit(1) 12 | sys.path.insert(0, '{}/tools'.format(SKOOLKIT_HOME)) 13 | from testwriter import write_tests 14 | 15 | SKOOL = 'jsw.skool' 16 | 17 | SNAPSHOT = 'build/jet_set_willy.z80' 18 | 19 | OUTPUT = """Using ref files: jsw.ref, bugs.ref, changelog.ref, facts.ref, glossary.ref, pokes.ref, sound.ref 20 | Parsing {skoolfile} 21 | Output directory: {odir}/jet_set_willy 22 | Copying {SKOOLKIT_HOME}/skoolkit/resources/skoolkit.css to skoolkit.css 23 | Copying jsw.css to jsw.css 24 | Writing disassembly files in asm 25 | Writing maps/all.html 26 | Writing maps/routines.html 27 | Writing maps/data.html 28 | Writing maps/messages.html 29 | Writing maps/unused.html 30 | Writing buffers/gbuffer.html 31 | Writing reference/bugs.html 32 | Writing reference/changelog.html 33 | Writing reference/facts.html 34 | Writing reference/glossary.html 35 | Writing reference/pokes.html 36 | Writing tables/rooms.html 37 | Writing tables/codes.html 38 | Writing reference/credits.html 39 | Writing sound/sound.html 40 | Writing index.html""" 41 | 42 | write_tests(SKOOL, SNAPSHOT, OUTPUT) 43 | -------------------------------------------------------------------------------- /sources/sound.ref: -------------------------------------------------------------------------------- 1 | ; Copyright 2022 Richard Dymond (rjdymond@gmail.com) 2 | 3 | [Page:Sound] 4 | SectionPrefix=Audio 5 | 6 | [Audio:themetune:Theme tune] 7 | This tune is played after the title screen has been prepared (see #R34984). 8 | 9 | #AUDIO(tune.wav)(#INCLUDE(ThemeTune)) 10 | 11 | [Audio:ingametune:In-game tune] 12 | This tune is played by the main loop while a game is in progress (see #R35644). 13 | 14 | #AUDIO(in-game-tune.wav)(#INCLUDE(InGameTune)) 15 | 16 | [Audio:screech:Title screen screech] 17 | This sound effect is produced after the theme tune has finished playing, and 18 | repeats until the 'Press ENTER to Start' message has finished scrolling across 19 | the screen (see #R34993). 20 | 21 | #PUSHS #POKES34276,192 #AUDIO4(screech.wav)(35033,35065) #POPS 22 | 23 | [Audio:item:Item] 24 | This sound effect is produced by the routine at #R37841 when Willy collects an 25 | item. 26 | 27 | #AUDIO(item.wav)(#FOR(128,4,-2,1)(c,1928-13*c)) 28 | 29 | [Audio:arrow:Arrow] 30 | This sound effect is produced when an arrow is fired (see #R37431). 31 | 32 | #AUDIO4(arrow.wav)(37455,37471) 33 | 34 | [Audio:life:Lose a life] 35 | This sound effect is produced by the routine at #R35841 when Willy loses a 36 | life. 37 | 38 | #AUDIO(die.wav)(#FOR(7,63,8,7)(d,[13*d+33]*(259-4*d),13*d+10895)) 39 | 40 | [Audio:gameover:Game over] 41 | This sound effect is produced by the routine at #R35914 during the game over 42 | sequence. 43 | 44 | #AUDIO(game-over.wav)(#GAMEOVER) 45 | 46 | [Audio:jumping:Jumping] 47 | These sound effects are produced by the routine at #R36307 when Willy is 48 | jumping. 49 | 50 | #AUDIO(jumping.wav)(#FOR1,18,,3(j,[104*(#IF(j>8)(j-8,8-j))+137]*31,280000)) 51 | 52 | [Audio:falling:Falling] 53 | These sound effects are produced when Willy is falling (see #R36458). 54 | 55 | #AUDIO(falling.wav)(#FOR3,31,,3(a,[208*(#IF(a<16)(a,a%4+12))+33]*31,280000)) 56 | -------------------------------------------------------------------------------- /sources/glossary.ref: -------------------------------------------------------------------------------- 1 | ; Copyright 2014, 2017, 2020, 2025 Richard Dymond (rjdymond@gmail.com) 2 | 3 | [Glossary:Animation frame index mask] 4 | The animation frame index mask for a guardian is found 5 | in bits 5-7 of byte 1 of its buffer (see #R33024) while it is on-screen, having 6 | been copied there from the corresponding entity 7 | definition (see #R40960). The mask determines the animation frames that the 8 | guardian's sprite cycles through. If 'B' is the guardian's base frame (0-7), 9 | then its sprite will cycle through animation frames as follows, where '|' 10 | denotes a bitwise '#S/OR/' operation (see #R37393): 11 | 12 | #TABLE(boxData,centre) 13 | { =h Mask | =h Frames } 14 | { 000 | B (1 frame) } 15 | { 001 | B, B|1 (1 or 2 distinct frames) } 16 | { 010 | B, B, B|2, B|2 (1 or 2 distinct frames) } 17 | { 011 | B, B|1, B|2, B|3 (1, 2 or 4 distinct frames) } 18 | { 100 | B, B, B, B, B|4, B|4, B|4, B|4 (1 or 2 distinct frames) } 19 | { 101 | B, B|1, B, B|1, B|4, B|5, B|4, B|5 (1, 2 or 4 distinct frames) } 20 | { 110 | B, B, B|2, B|2, B|4, B|4, B|6, B|6 (1, 2 or 4 distinct frames) } 21 | { 111 | B, B|1, B|2, B|3, B|4, B|5, B|6, B|7 (1, 2, 4 or 8 distinct frames) } 22 | TABLE# 23 | 24 | For example, if a guardian's base frame is 2 and its animation frame index mask 25 | is 011, then its sprite will cycle through frames 2, 2|1, 2|2 and 2|3 (i.e. 26 | frames 2 and 3). 27 | 28 | Note that the animation frame masks 100, 101 and 110 are not used by any 29 | guardians in the game, and no guardian has a base frame of 7. 30 | 31 | [Glossary:Entity] 32 | A guardian, arrow or rope. 33 | 34 | [Glossary:Entity definition] 35 | One of the 128 8-byte blocks located at #R40960 onwards. Each one defines the 36 | location and movement characteristics of a guardian, arrow or rope. 37 | 38 | [Glossary:Entity specification] 39 | One of the eight pairs of bytes at the end of a room definition (e.g. at 40 | #R53488). Each one specifies a guardian, arrow or rope present in the room. 41 | 42 | [Glossary:Guardian] 43 | An inhabitant of a room that continuously moves left and right (horizontal 44 | guardian) or up and down (vertical guardian) in a straight line, or 45 | #FACT(guardianOrFixture)(stays still). Contact with a guardian is fatal. Some 46 | examples are: 47 | 48 | #UDGTABLE(gallery) 49 | { #R50432(The Security Guard) | #GUARDIAN191,4,66 } 50 | { #R54016(The Forgotten Abbey) | #GUARDIAN180,2,6 } 51 | { #R57600(The Bathroom) | #GUARDIAN185,1,4 } 52 | TABLE# 53 | 54 | The graphic data for all the guardians in the game can be found at #R43776. 55 | 56 | [Glossary:Nasty] 57 | An immobile object drawn in a single cell as part of a room along with the 58 | background, walls, floors, ramp and conveyor. Contact with a nasty is fatal. 59 | Some examples are: 60 | 61 | #UDGTABLE(gallery) 62 | { #R51968(The Hall) | #UDG52156,68(nasty11) } 63 | { #R55552(Cold Store) | #UDG55740,79(nasty25) } 64 | { #R61696(The Wine Cellar) | #UDG61884,70(nasty49) } 65 | TABLE# 66 | -------------------------------------------------------------------------------- /utils/build-disassembly: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | D_VERSIONS=( 4 | 20221122 5 | 20200806 6 | 20190607 7 | 20181016 8 | 20171113 9 | 20170518 10 | 20161105 11 | 20160511 12 | 20160117 13 | 20150328 14 | 20141114 15 | 20140913 16 | 20140807 17 | ) 18 | 19 | declare -A SK_VERSIONS 20 | SK_VERSIONS=( 21 | [20131102]=3.6 22 | [20130901]=3.5 23 | [20121101]=3.2 24 | [20120501]=3.0.2 25 | [20120411]=3.0.1 26 | ) 27 | 28 | JSWZ80=$JETSETWILLY_HOME/build/jet_set_willy.z80 29 | 30 | if (($# < 2)); then 31 | echo "Usage: $(basename $0) VERSION SKOOLKIT_VERSION" 32 | echo "" 33 | echo " Attempts to build a specific version of the Jet Set Willy disassembly using" 34 | echo " a specific version of SkoolKit." 35 | echo "" 36 | echo " VERSION must be one of:" 37 | echo "" 38 | for v in ${D_VERSIONS[@]} ${!SK_VERSIONS[@]}; do 39 | echo " $v" 40 | done | sort -r | column -c 80 41 | echo "" 42 | echo " Environment variables:" 43 | echo " SKOOLKIT_ARCHIVE - directory containing SkoolKit release tarballs" 44 | echo " DISASSEMBLY_ARCHIVE - directory containing disassembly release tarballs" 45 | exit 1 46 | fi 47 | 48 | if [[ -z $SKOOLKIT_ARCHIVE ]]; then 49 | echo 'SKOOLKIT_ARCHIVE is not set; aborting' 50 | exit 1 51 | fi 52 | if [[ ! -d $SKOOLKIT_ARCHIVE ]]; then 53 | echo "SKOOLKIT_ARCHIVE=$SKOOLKIT_ARCHIVE: directory not found" 54 | exit 1 55 | fi 56 | 57 | D_VERSION=$1 58 | SK_VERSION=$2 59 | 60 | SK_VERSION_INT=$(echo $SK_VERSION | tr -d . )0 61 | SK_VERSION_INT=${SK_VERSION_INT:0:3} 62 | 63 | if (($SK_VERSION_INT < 300)); then 64 | echo "Building the Jet Set Willy disassembly is not supported with SkoolKit < 3.0" 65 | exit 1 66 | fi 67 | 68 | odir=jsw-$D_VERSION-$SK_VERSION 69 | rm -rf $odir 70 | mkdir $odir 71 | cd $odir 72 | 73 | SK_DIR=skoolkit-$SK_VERSION 74 | tar xf $SKOOLKIT_ARCHIVE/$SK_DIR.tar.* 75 | SK_BUILD_DIR=${SK_DIR}-build 76 | mv $SK_DIR $SK_BUILD_DIR 77 | 78 | HTML_OPTS="" 79 | if (($D_VERSION >= 20140913)); then 80 | if [[ -z $DISASSEMBLY_ARCHIVE ]]; then 81 | echo 'DISASSEMBLY_ARCHIVE is not set; aborting' 82 | exit 1 83 | fi 84 | if [[ ! -d $DISASSEMBLY_ARCHIVE ]]; then 85 | echo "DISASSEMBLY_ARCHIVE=$DISASSEMBLY_ARCHIVE: directory not found" 86 | exit 1 87 | fi 88 | D_DIR=jsw-disassembly-$D_VERSION 89 | tar xf $DISASSEMBLY_ARCHIVE/$D_DIR.tar.xz 90 | [[ -d $D_DIR/src ]] && SOURCES_DIR=src || SOURCES_DIR=sources 91 | cd $SK_BUILD_DIR 92 | cp -p ../$D_DIR/$SOURCES_DIR/*.{skool,ref,css} . 93 | find ../$D_DIR -name jetsetwilly.py -exec cp -p {} . \; 94 | HTML_OPTS="-W jetsetwilly.JetSetWillyHtmlWriter" 95 | elif (($D_VERSION == 20140807)); then 96 | wget https://github.com/skoolkid/jetsetwilly/archive/${D_VERSION}.tar.gz -O - | tar xzf - 97 | D_DIR=jetsetwilly-$D_VERSION 98 | cd $SK_BUILD_DIR 99 | ../$D_DIR/utils/jsw2ctl.py $JSWZ80 > jet_set_willy.ctl 100 | cp -a ../$D_DIR/jet_set_willy.ref . 101 | cp -a ../$D_DIR/resources/jet_set_willy*.css . 102 | ./sna2skool.py -c jet_set_willy.ctl $JSWZ80 > jet_set_willy.skool 103 | cp -p ../$D_DIR/skoolkit/jetsetwilly.py skoolkit 104 | else 105 | D_SK_VERSION=${SK_VERSIONS[$D_VERSION]} 106 | if [[ -z $D_SK_VERSION ]]; then 107 | echo "Disassembly version not recognised: $D_VERSION" 108 | exit 1 109 | fi 110 | D_SK_VERSION_INT=$(echo $D_SK_VERSION | tr -d . )0 111 | D_SK_VERSION_INT=${D_SK_VERSION_INT:0:3} 112 | D_SK_DIR=skoolkit-$D_SK_VERSION 113 | tar xf $SKOOLKIT_ARCHIVE/$D_SK_DIR.tar.* 114 | D_DIR=$D_SK_DIR-src 115 | mv $D_SK_DIR $D_DIR 116 | cd $SK_BUILD_DIR 117 | cp -a ../$D_DIR/examples/jet_set_willy.{ctl,ref} . 118 | cp -a ../$D_DIR/examples/jet_set_willy*.css . &>/dev/null || true 119 | mv examples examples-$SK_VERSION 120 | mkdir examples 121 | cp -a jet_set_willy*.css examples &>/dev/null || true 122 | ./sna2skool.py -c jet_set_willy.ctl $JSWZ80 > jet_set_willy.skool 123 | cp -p ../$D_DIR/skoolkit/jetsetwilly.py skoolkit &>/dev/null || true 124 | fi 125 | 126 | HTML_OPTS="$HTML_OPTS -d .." 127 | if (($SK_VERSION_INT >= 302)); then 128 | HTML_OPTS="$HTML_OPTS -t" 129 | fi 130 | [ -f jsw.skool ] && JSW_SKOOL=jsw.skool || JSW_SKOOL=jet_set_willy.skool 131 | ./skool2html.py $HTML_OPTS $JSW_SKOOL 132 | -------------------------------------------------------------------------------- /utils/jswimage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | import argparse 5 | 6 | SKOOLKIT_HOME = os.environ.get('SKOOLKIT_HOME') 7 | if not SKOOLKIT_HOME: 8 | sys.stderr.write('SKOOLKIT_HOME is not set; aborting\n') 9 | sys.exit(1) 10 | if not os.path.isdir(SKOOLKIT_HOME): 11 | sys.stderr.write('SKOOLKIT_HOME={}; directory not found\n'.format(SKOOLKIT_HOME)) 12 | sys.exit(1) 13 | sys.path.insert(0, SKOOLKIT_HOME) 14 | 15 | JETSETWILLY_HOME = os.environ.get('JETSETWILLY_HOME') 16 | if not JETSETWILLY_HOME: 17 | sys.stderr.write('JETSETWILLY_HOME is not set; aborting\n') 18 | sys.exit(1) 19 | if not os.path.isdir(JETSETWILLY_HOME): 20 | sys.stderr.write('JETSETWILLY_HOME={}; directory not found\n'.format(JETSETWILLY_HOME)) 21 | sys.exit(1) 22 | sys.path.insert(0, '{}/sources'.format(JETSETWILLY_HOME)) 23 | 24 | from skoolkit.image import ImageWriter 25 | from skoolkit.refparser import RefParser 26 | from skoolkit.skoolhtml import Frame 27 | from skoolkit.snapshot import get_snapshot 28 | from jetsetwilly import JetSetWillyHtmlWriter 29 | 30 | class JetSetWilly(JetSetWillyHtmlWriter): 31 | def __init__(self, snapshot): 32 | self.snapshot = snapshot 33 | self.defaults = RefParser() 34 | self.ref_parser = RefParser() 35 | self.ref_parser.parse('{}/sources/jsw.ref'.format(JETSETWILLY_HOME)) 36 | self.init() 37 | 38 | def _do_pokes(specs, snapshot): 39 | for spec in specs: 40 | addr, val = spec.split(',', 1) 41 | step = 1 42 | if '-' in addr: 43 | addr1, addr2 = addr.split('-', 1) 44 | addr1 = int(addr1) 45 | if '-' in addr2: 46 | addr2, step = [int(i) for i in addr2.split('-', 1)] 47 | else: 48 | addr2 = int(addr2) 49 | else: 50 | addr1 = int(addr) 51 | addr2 = addr1 52 | addr2 += 1 53 | value = int(val) 54 | for a in range(addr1, addr2, step): 55 | snapshot[a] = value 56 | 57 | def _place_willy(jsw, room, spec): 58 | room_addr = 49152 + 256 * room 59 | udg_array = jsw._get_room_udgs(room_addr) 60 | if spec: 61 | values = [] 62 | for n in spec.split(','): 63 | try: 64 | values.append(int(n)) 65 | except ValueError: 66 | values.append(None) 67 | values += [None] * (3 - len(values)) 68 | x, y, frame = values 69 | if x is not None and y is not None: 70 | willy = jsw._get_graphic(40192 + 32 * (frame or 0), 7) 71 | bg_attr = jsw.snapshot[room_addr + 160] 72 | jsw._place_graphic(udg_array, willy, x, y * 8, bg_attr) 73 | return udg_array 74 | 75 | def run(imgfname, options): 76 | snapshot = get_snapshot('{}/build/jet_set_willy.z80'.format(JETSETWILLY_HOME)) 77 | _do_pokes(options.pokes, snapshot) 78 | jsw = JetSetWilly(snapshot) 79 | udg_array = _place_willy(jsw, options.room, options.willy) 80 | if options.geometry: 81 | wh, xy = options.geometry.split('+', 1) 82 | width, height = [int(n) for n in wh.split('x')] 83 | x, y = [int(n) for n in xy.split('+')] 84 | udg_array = [row[x:x + width] for row in udg_array[y:y + height]] 85 | frame = Frame(udg_array, options.scale) 86 | image_writer = ImageWriter() 87 | with open(imgfname, "wb") as f: 88 | image_writer.write_image([frame], f) 89 | 90 | ############################################################################### 91 | # Begin 92 | ############################################################################### 93 | parser = argparse.ArgumentParser( 94 | usage='jswimage.py [options] FILE.png', 95 | description="Create an image of a room in Jet Set Willy.", 96 | add_help=False 97 | ) 98 | parser.add_argument('imgfname', help=argparse.SUPPRESS, nargs='?') 99 | group = parser.add_argument_group('Options') 100 | group.add_argument('-g', dest='geometry', metavar='WxH+X+Y', 101 | help='Create an image with this geometry') 102 | group.add_argument('-p', dest='pokes', metavar='A[-B[-C]],V', action='append', default=[], 103 | help="Do POKE N,V for N in {A, A+C, A+2C,...B} (this option may be used multiple times)") 104 | group.add_argument('-r', dest='room', type=int, default=0, 105 | help='Create an image of this room (default: 0)') 106 | group.add_argument('-s', dest='scale', type=int, default=2, 107 | help='Set the scale of the image (default: 2)') 108 | group.add_argument('-w', dest='willy', metavar='X,Y[,F]', 109 | help="Place Willy at (X,Y) with animation frame F (0-7)") 110 | namespace, unknown_args = parser.parse_known_args() 111 | if unknown_args or not namespace.imgfname: 112 | parser.exit(2, parser.format_help()) 113 | run(namespace.imgfname, namespace) 114 | -------------------------------------------------------------------------------- /sources/jetsetwilly.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012, 2014-2022 Richard Dymond (rjdymond@gmail.com) 2 | # 3 | # This program is free software: you can redistribute it and/or modify it under 4 | # the terms of the GNU General Public License as published by the Free Software 5 | # Foundation, either version 3 of the License, or (at your option) any later 6 | # version. 7 | # 8 | # This program is distributed in the hope that it will be useful, but WITHOUT 9 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 10 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 11 | # details. 12 | # 13 | # You should have received a copy of the GNU General Public License along with 14 | # this program. If not, see . 15 | 16 | from skoolkit.graphics import Frame, Udg 17 | from skoolkit.skoolhtml import HtmlWriter 18 | from skoolkit.skoolmacro import parse_ints, parse_brackets, parse_image_macro 19 | 20 | class JetSetWillyHtmlWriter(HtmlWriter): 21 | def init(self): 22 | self.font = {c: self.snapshot[15360 + 8 * c:15368 + 8 * c] for c in range(32, 122)} 23 | self.room_frames = {} 24 | 25 | def expand_rframe(self, text, index, cwd): 26 | # #RFRAME(num,force=0,fix=0)(frame=$num) 27 | end, num, force, fix = parse_ints(text, index, 0, (0, 0), ('num', 'force', 'fix'), self.fields) 28 | if force: 29 | end, frame = parse_brackets(text, end) 30 | else: 31 | frame = str(num) 32 | if force or num not in self.room_frames: 33 | udgs = self._get_room_udgs(49152 + num * 256, fix) 34 | self.handle_image(Frame(udgs, 2, name=frame)) 35 | if not force: 36 | self.room_frames[num] = True 37 | return end, '' 38 | 39 | def _build_logo(self): 40 | udgs = [] 41 | for j in range(38944, 39424, 32): 42 | row = [] 43 | for i in range(j + 3, j + 29): 44 | attr = self.snapshot[i] 45 | if attr in (5, 8, 41, 44): 46 | udg_addr = 33841 + 8 * (i & 1) 47 | elif attr in (4, 37, 40): 48 | udg_addr = 33857 + 8 * (i & 1) 49 | else: 50 | udg_addr = 15616 51 | if attr == 44: 52 | attr = 37 53 | row.append(Udg(attr & 127, self.snapshot[udg_addr:udg_addr + 8])) 54 | udgs.append(row) 55 | udgs.append([Udg(0, (0,) * 8)] * len(udgs[0])) 56 | return udgs 57 | 58 | def expand_jsw(self, text, index, cwd): 59 | # #JSWtrans[{x,y,width,height}](fname) 60 | end, crop_rect, fname, frame, alt, (trans,) = parse_image_macro(text, index, names=['trans']) 61 | tindex = int(trans > 0) 62 | alpha = 255 * int(trans == 0) 63 | udgs = lambda: self._build_logo() 64 | frames = [Frame(udgs, 1, 0, *crop_rect, name=frame, tindex=tindex, alpha=alpha)] 65 | return end, self.handle_image(frames, fname, cwd, alt) 66 | 67 | def _get_room_udgs(self, addr, fix=0): 68 | # Collect block graphics 69 | block_graphics = [] 70 | for a in range(addr + 160, addr + 206, 9): 71 | attr = self.snapshot[a] 72 | if fix: 73 | b = a 74 | else: 75 | # Simulate the 'Cell-Graphics' bug 76 | # https://www.oocities.org/andrewbroad/spectrum/willy/bugs.html 77 | b = addr + 160 78 | while b < a and self.snapshot[b] != attr: 79 | b += 1 80 | block_graphics.append(Udg(attr, self.snapshot[b + 1:b + 9])) 81 | room_bg = block_graphics[0].attr 82 | 83 | # Build the room UDG array 84 | udg_array = [] 85 | for a in range(addr, addr + 128): 86 | if a % 8 == 0: 87 | udg_array.append([]) 88 | b = self.snapshot[a] 89 | for block_id in (b >> 6, (b >> 4) & 3, (b >> 2) & 3, b & 3): 90 | udg_array[-1].append(block_graphics[block_id].copy()) 91 | 92 | # Room name 93 | name_udgs = [Udg(70, self.font[b]) for b in self.snapshot[addr + 128:addr + 160]] 94 | udg_array.append(name_udgs) 95 | 96 | # Ramp 97 | direction, p1, p2, length = self.snapshot[addr + 218:addr + 222] 98 | if length: 99 | attr = self.snapshot[addr + 196] 100 | ramp_udg = block_graphics[4] 101 | direction = direction * 2 - 1 102 | x = p1 & 31 103 | y = 8 * (p2 & 1) + (p1 & 224) // 32 104 | for i in range(length): 105 | udg_array[y][x] = ramp_udg.copy() 106 | y -= 1 107 | x += direction 108 | 109 | # Conveyor 110 | p1, p2, length = self.snapshot[addr + 215:addr + 218] 111 | if length: 112 | conveyor_udg = block_graphics[5] 113 | x = p1 & 31 114 | y = 8 * (p2 & 1) + (p1 & 224) // 32 115 | for i in range(x, x + length): 116 | udg_array[y][i] = conveyor_udg.copy() 117 | 118 | return udg_array 119 | -------------------------------------------------------------------------------- /sources/pokes.ref: -------------------------------------------------------------------------------- 1 | ; Copyright 2014-2017, 2021, 2024, 2025 Richard Dymond (rjdymond@gmail.com) 2 | 3 | [Poke:noCodeSheetRequired:No code sheet required] 4 | To make any combination work on the code entry screen: 5 | 6 | POKE #R34483(34483),195 7 | 8 | Alternatively, to bypass the code entry screen altogether: 9 | 10 | POKE #R33796(33797),135: POKE #R33799(33800),202 11 | 12 | [Poke:startInAnyRoom:Start in any room] 13 | To start the game in room N (normally #b33: #R57600(The Bathroom)): 14 | 15 | POKE #R34794(34795),N 16 | 17 | For a list of all the rooms and their numbers, see the #LINK(Rooms)(Rooms 18 | page). 19 | 20 | The default start location, (13,20), may not be suitable, depending on the room 21 | chosen; to change Willy's start location to (y,x): 22 | 23 |
POKE #R34789(34790),y*16
24 |
POKE #R34799(34800),32*(y-8*INT(y/8))+x
25 |
POKE #R34799(34801),92+INT(y/8)
26 | 27 | For example, to start at (6,28) in room #b25 (#R55552(Cold Store)): 28 | 29 |
POKE 34795,25
30 |
POKE 34790,96
31 |
POKE 34800,220
32 |
POKE 34801,92
33 | 34 | [Poke:gentleGuardians:Gentle guardians] 35 | To enable Willy to walk through (or rather, behind) guardians with impunity: 36 | 37 | POKE #R37391(37392),0 38 | 39 | [Poke:niceNasties:Nice nasties] 40 | To enable Willy to survive contact with nasties: 41 | 42 | POKE #R38447(38447),201 43 | 44 | [Poke:agreeableArrows:Agreeable arrows] 45 | To enable Willy to survive contact with arrows: 46 | 47 | POKE #R37505(37505),24 48 | 49 | [Poke:harmlessHeights:Harmless heights] 50 | To enable Willy to survive a fall from any height: 51 | 52 | POKE #R36577(36578),16 53 | 54 | [Poke:noConveyors:No conveyors] 55 | To enable Willy to walk freely in any direction on conveyors: 56 | 57 | POKE #R36601(36601),0 58 | 59 | [Poke:dealWithMaria:Deal with Maria] 60 | To get rid of Maria entirely, leaving the bedroom free to enter without 61 | collecting any items: 62 | 63 | POKE #R38207(38207),24 64 | 65 | Or if that makes things a little too easy, try the following POKE that gets rid 66 | of Maria after N items have been collected: 67 | 68 | POKE #R41983(41983),256-N (1<=N<=83) 69 | 70 | Exactly which N items need to be collected is determined by the item table at 71 | #R41984. When N=1, the only item that needs to be collected is the one in 72 | #R57600(The Bathroom). 73 | 74 | [Poke:cheatModeCheat:Cheat mode cheat] 75 | To activate the #FACT(writetyper)(WRITETYPER cheat mode) immediately (without 76 | having to type WRITETYPER in #R56320(First Landing)): 77 | 78 | POKE #R34275(34275),10 79 | 80 | For a list of all the rooms and their teleport codes, see the 81 | #LINK(Rooms)(Rooms page). 82 | 83 | [Poke:moreLives:More lives] 84 | To start the game with N lives remaining (0<=N<=255; normally N=7): 85 | 86 | POKE #R34784(34785),N 87 | 88 | Note that the routine at #R35211 tries to draw all remaining lives at the 89 | bottom of the screen, which means that for large N, there is screen corruption 90 | and the game slows down considerably; to avoid this, disable the routine: 91 | 92 | POKE #R35211(35211),201 93 | 94 | [Poke:infiniteLives:Infinite lives] 95 | To give Willy infinite lives: 96 | 97 | POKE #R35899(35899),0 98 | 99 | [Poke:infiniteTime:Infinite time] 100 | To stop the clock and give Willy infinite time to complete his task: 101 | 102 | POKE #R35408(35408),24 103 | 104 | [Poke:theOfficialSoftwareProjectsPOKEs:The official Software Projects POKEs] 105 | When it became clear that the #BUG(theAtticBug)(Attic bug) was actually a bug 106 | and not a feature, Software Projects published some POKEs to fix it and some 107 | other problems with the game: 108 | 109 |
POKE #R60160#60160(60231),0
110 |
POKE #R42183(42183),11
111 |
POKE #R59900(59901),82
112 |
POKE #R56832#56832(56876),4
113 | 114 | The first POKE (#R60160#60160(60231),0) removes a nasty from 115 | #R60160(Conservatory Roof), which makes it possible to collect the otherwise 116 | #BUG(theUncollectableItem)(uncollectable item) on the far right: 117 | 118 | #UDGTABLE(gallery) 119 | { #ROOM43,2,16,4,16,12(sp_poke1_before) | 120 | #PUSHS #POKES60231,0 #ROOM43,2,16,4,16,12,5(sp_poke1_after) #POPS } 121 | { Before | After POKE #R60160#60160(60231),0 } 122 | TABLE# 123 | 124 | The second POKE (#R42183(42183),11) moves the unreachable 125 | #BUG(theInvisibleItem)(invisible item) at (3,26) in #R56320(First Landing) to 126 | the same coordinates in #R51968(The Hall) (where it is still invisible, but now 127 | reachable). 128 | 129 | The third POKE (#R59900(59901),82) fixes the misplaced arrow in #R59648(The 130 | Attic) by setting its pixel y-coordinate to 41. 131 | 132 | The fourth POKE (#R56832#56832(56876),4) replaces a wall block with a floor 133 | block in #R56832(The Banyan Tree), which makes it possible to access the room 134 | above (#R58368(A bit of tree)) from the right-hand side (which in turn makes it 135 | possible to reach the otherwise #BUG(theInaccessibleItems)(inaccessible items) 136 | in #R60160(Conservatory Roof) via the ledge on the lower left in 137 | #R59904(Under the Roof)): 138 | 139 | #UDGTABLE(gallery) 140 | { #ROOM30,2,8,0,16,13(sp_poke4_before) | 141 | #PUSHS #POKES56876,4 #ROOM30,2,8,0,16,13,5(sp_poke4_after) #POPS } 142 | { Before | After POKE #R56832#56832(56876),4 } 143 | TABLE# 144 | -------------------------------------------------------------------------------- /sources/changelog.ref: -------------------------------------------------------------------------------- 1 | ; Copyright 2014-2020, 2022, 2025 Richard Dymond (rjdymond@gmail.com) 2 | 3 | [Page:Changelog] 4 | SectionType=BulletPoints 5 | 6 | [Changelog:20221122] 7 | - 8 | 9 | - Added the 'Graphics and sound' section to the #LINK(GameIndex)(home page) 10 | - Added the #LINK(Sound)() page 11 | 12 | [Changelog:20200806] 13 | - 14 | 15 | - In #BUG(theStickyBed)(), added a POKE to make the bed unsticky (thanks IRF) 16 | 17 | [Changelog:20190607] 18 | - 19 | 20 | - Added the #IF({vars[pub]}==1)() 21 | hexadecimal version of the disassembly#IF({vars[pub]}==1)() to the 22 | distribution 23 | - Replaced GIF images with PNG images 24 | 25 | [Changelog:20181016] 26 | - 27 | 28 | - Ported the disassembly to SkoolKit 7 29 | - Documented a bug: #BUG(corruptedNasties)() (thanks IRF) 30 | - In #FACT(theEncroachingRope)(), added a note about the danger of having a 31 | rope defined as the eighth entity in a room (thanks IRF) 32 | 33 | [Changelog:20171113] 34 | - 35 | 36 | - Added trivia entries: 37 | - #FACT(flickeringClock)() (thanks Spider) 38 | - #FACT(unanimatedConveyors)() 39 | - In #FACT(guardianOrFixture)(), added a note about the minimum and maximum 40 | pixel y-coordinates 41 | - In #BUG(dontMindYourHead)(), added a note about collecting the item in 42 | #R54016(The Forgotten Abbey) (thanks IRF) 43 | 44 | [Changelog:20170518] 45 | - 46 | 47 | - Ported the disassembly to SkoolKit 6 48 | - Fixed comments in the entry at #R36796 with regard to ramps (thanks IRF) 49 | - Documented a bug: #BUG(missedNote)() (thanks IRF) 50 | - Made more improvements to the hexadecimal version of the disassembly 51 | 52 | [Changelog:20161105] 53 | - 54 | 55 | - Added trivia entries: 56 | - #FACT(floorRamps)() (thanks IRF) 57 | - #FACT(disrespectfulMonk)() (thanks IRF) 58 | - #FACT(uneconomicalUnderworld)() (thanks IRF) 59 | - #FACT(underanimatedGuardians)() (thanks IRF) 60 | - #FACT(rampsVsWalls)() (thanks IRF) 61 | - In #FACT(oneWaySaw)(), added a POKE to make the saw face right when moving in 62 | that direction 63 | - In #FACT(unusedRoom)(), added a note about the right exit from #R60928(Tree 64 | Root) (thanks IRF) 65 | - In #BUG(veryCorruptedConveyor)(), added a note about fixing the appearance of 66 | the ramp tile (thanks IRF) 67 | - On the #LINK(UnusedMap)() page, added a list of unused areas embedded in code 68 | and data blocks 69 | - Added a glossary entry: #LINK(Glossary#animation_frame_index_mask)(Animation 70 | frame index mask) 71 | 72 | [Changelog:20160511] 73 | - 74 | 75 | - Added trivia entries: 76 | - #FACT(doubleDescent)() (thanks IRF) 77 | - #FACT(itemsAndWhiteness)() (thanks IRF) 78 | - #FACT(oneWaySaw)() 79 | - #FACT(mostGuardians)() 80 | - #FACT(quadridirectionalGuardian)() 81 | - #FACT(unanimatedGuardians)() 82 | - #FACT(highJump)() 83 | - Documented bugs: 84 | - #BUG(veryCorruptedConveyor)() (thanks Stuart Brady and IRF) 85 | - #BUG(fromTopToBottom)() (thanks Spider, IRF and jetsetdanny) 86 | - #BUG(longDistanceNasties)() (thanks IRF) 87 | - #BUG(stuckInTheWall)() (thanks IRF) 88 | - #BUG(guardianHalos)() 89 | - In #FACT(guardiansNeedAClearPath)(), added a note about arrows hitting 90 | guardians (thanks IRF) 91 | - The table of #R40960(entity definitions) now shows every animation frame for 92 | each guardian 93 | - Made various improvements to the hexadecimal version of the disassembly 94 | 95 | [Changelog:20160117] 96 | - 97 | 98 | - In #FACT(theMusicOfLife)(), added a POKE to prevent the in-game music from 99 | deteriorating as Willy loses lives 100 | - In #BUG(theStickyBed)(), added a note about using 'right' keys other than 'P' 101 | to move Willy off the bed (thanks jetsetdanny) 102 | - In #BUG(selfCollectingItem)(), added a POKE that enables Willy to collect the 103 | item himself (thanks Stuart Brady) 104 | - In #FACT(gameOverAt1am)(), added a note about the message that scrolls across 105 | the lower part of the title screen 106 | - In #BUG(corruptedConveyors)(), added POKEs that fix how conveyors are drawn 107 | - In #FACT(oneItemBehindAnother)(), added a POKE that moves one of the items to 108 | a different location (thanks Stuart Brady) 109 | 110 | [Changelog:20150328] 111 | - 112 | 113 | - Added a POKE: #POKE(infiniteTime)() 114 | 115 | [Changelog:20141114] 116 | - 117 | 118 | - Added trivia entries: 119 | - #FACT(tunes)() 120 | - #FACT(unusedRoom)() 121 | - #FACT(codesFromCode)() 122 | - #FACT(someAirLeft)() 123 | - #FACT(drJonesWillNeverBelieveThis)() (thanks Dunny) 124 | - Added animated images to the #BUG(corruptedConveyors)() bug 125 | 126 | [Changelog:20140913] 127 | The disassembly is now 'complete'. 128 | 129 | - Annotated all the #LINK(RoutinesMap)(routines) 130 | - Annotated every byte of the entity definitions at #R40960 131 | - Added the #LINK(Rooms)(Rooms page) 132 | - Added trivia entries: 133 | - #FACT(gameOverAt1am)() 134 | - #FACT(theMovingBed)() 135 | - #FACT(theMusicOfLife)() 136 | - #FACT(takingABreak)() 137 | - #FACT(writetyper)() 138 | - #FACT(asYouWere)() 139 | - #FACT(whiteSeekingMissile)() 140 | - #FACT(beerInTheWall)() 141 | - #FACT(dangerousConnections)() 142 | - #FACT(commonestItem)() 143 | - #FACT(commonestCodes)() 144 | - #FACT(mariasDodgyDepthPerception)() 145 | - #FACT(ropeClimbingForBeginners)() 146 | - #FACT(theEncroachingRope)() 147 | - #FACT(ropesBeforeArrows)() 148 | - #FACT(guardianColours)() 149 | - #FACT(guardiansNeedAClearPath)() 150 | - #FACT(guardianOrFixture)() 151 | - #FACT(unusedEntityDefinition)() 152 | - #FACT(untouchableEntityDefinition)() 153 | - #FACT(oneItemBehindAnother)() 154 | - #FACT(slipperySlopes)() 155 | - #FACT(toPauseOrNotToPause)() 156 | - #FACT(cheatsNeverCycleColours)() 157 | - #FACT(nomenLuni)() 158 | - #FACT(weMustPerformAQuirkafleeg)() 159 | - #FACT(rescueEsmerelda)() 160 | - Documented bugs: 161 | - #BUG(faultyClock)() 162 | - #BUG(theStickyBed)() 163 | - #BUG(selfCollectingItem)() 164 | - #BUG(theInvisibleItem)() 165 | - #BUG(theInaccessibleItems)() 166 | - #BUG(theUncollectableItem)() 167 | - #BUG(dontMindYourHead)() 168 | - #BUG(theBlankConveyor)() 169 | - #BUG(corruptedConveyors)() 170 | - #BUG(thePauseBug)() 171 | - #BUG(theAtticBug)() 172 | - Added POKEs: 173 | - #POKE(dealWithMaria)() 174 | - #POKE(startInAnyRoom)() 175 | - #POKE(gentleGuardians)() 176 | - #POKE(niceNasties)() 177 | - #POKE(agreeableArrows)() 178 | - #POKE(harmlessHeights)() 179 | - #POKE(noConveyors)() 180 | - #POKE(cheatModeCheat)() 181 | - #POKE(theOfficialSoftwareProjectsPOKEs)() 182 | 183 | [Changelog:20140807] 184 | - 185 | 186 | - Added trivia entries: 187 | - #FACT(unusedGridLocation)() 188 | - #FACT(unusedGuardian)() 189 | - #FACT(uniqueGuardians)() 190 | - #FACT(commonestGuardian)() 191 | - Documented a bug: #BUG(invalidGridLocation)() 192 | - Added POKEs: 193 | - #POKE(noCodeSheetRequired)() 194 | - #POKE(moreLives)() 195 | - #POKE(infiniteLives)() 196 | - Added routine titles 197 | - Added a description to each #LINK(GameStatusBuffer)(game status buffer 198 | entry), #LINK(MessagesMap)(message entry) and #LINK(DataMap)(data block) 199 | listing the routines that use it 200 | - Added item locations to the entry at #R41984 201 | - Listed the rooms in which each guardian appears (see #R40000, #R40064 and 202 | #R43776) 203 | - Listed the rooms in which each entity definition is used (see #R40960) 204 | - Added the code for each grid location to the entry at #R40448 205 | 206 | [Changelog:20131102] 207 | Included in SkoolKit 3.6. 208 | 209 | - Added animated images of Willy to the entry at #R40192 210 | - Added animated images of the toilet to the entry at #R42496 211 | 212 | [Changelog:20130901] 213 | Included in SkoolKit 3.5. 214 | 215 | - Annotated the source code remnants at #R39936 216 | 217 | [Changelog:20121101] 218 | Included in SkoolKit 3.2. 219 | 220 | - Added the first seven trivia entries: 221 | - #FACT(unusedItems)() 222 | - #FACT(unusedNasties)() 223 | - #FACT(unusedFloor)() 224 | - #FACT(unusedRamp)() 225 | - #FACT(unusedConveyor)() 226 | - #FACT(emptiestRooms)() 227 | - #FACT(mostItems)() 228 | 229 | [Changelog:20120501] 230 | Included in SkoolKit 3.0.2. 231 | 232 | - Added room images and descriptions 233 | 234 | [Changelog:20120411] 235 | Initial version, included in SkoolKit 3.0.1. 236 | -------------------------------------------------------------------------------- /sources/bugs.ref: -------------------------------------------------------------------------------- 1 | ; Copyright 2014-2017, 2019, 2021, 2025 Richard Dymond (rjdymond@gmail.com) 2 | 3 | [Bug:faultyClock:12:30am in the afternoon] 4 | The clock-updating code at #R35410 doesn't switch from 'am' to 'pm' until 1pm 5 | (instead of 12 noon), and also doesn't switch from 'pm' to 'am' at midnight. 6 | 7 | [Bug:theStickyBed:The sticky bed] 8 | After collecting all the items, if Willy jumps onto the bed in #R58112(Master 9 | Bedroom) (which is a conveyor moving right) instead of walking up to the foot 10 | of it, he will stop moving and the 'P' (right) key becomes unresponsive. This 11 | happens because the code at #R36619 flips bit 0 of #REGe - which has already 12 | been reset because Willy is on a conveyor moving right - to 1, thus preventing 13 | the conveyor from moving Willy, and also effectively disabling the 'P' key. 14 | 15 | To see this, use POKE #R41983(41983),255 to make the tap in #R57600(The 16 | Bathroom) the only item that needs to be collected to complete the game, make 17 | your way to the bedroom, and jump onto the bed. 18 | 19 | However, all is not lost. Willy can be moved off the bed by using one of the 20 | other 'right' keys: 'I', 'Y', 'R', 'W', '7' or '8'. 21 | 22 | To make the bed unsticky: 23 | 24 | POKE #R58326(58326),254 25 | 26 | [Bug:selfCollectingItem:The self-collecting item] 27 | The item in #R57088(Swimming Pool) is automatically collected as soon as Willy 28 | enters the room; this is because the room's background tile has white INK, and 29 | the routine at #R37841 collects any item that is drawn on a cell with white 30 | INK. There are no other rooms whose background tile has white INK. 31 | 32 | To change the INK of the room's background tile from white to cyan, enabling 33 | Willy to collect the item himself: 34 | 35 | POKE #R57248(57248),5 36 | 37 | Note that this changes the rope's colour from white to cyan. 38 | 39 | [Bug:theInvisibleItem:The invisible item] 40 | There is an item in #R56320(First Landing), but you could be forgiven for not 41 | knowing because it's invisible. To give the item a shape and make it visible: 42 | 43 | POKE 56548,24: POKE 56549,24 44 | 45 | But now that the item is revealed, we can also see that it's unreachable: 46 | 47 | #PUSHS 48 | #POKES56548,24,2 49 | #ROOM28,2,16,0,16,16,5(first_landing_item1) 50 | 51 | To add a ledge and open the wall to make the item reachable: 52 | 53 | POKE 56350,0: POKE 56358,0: POKE 56365,170 54 | 55 | #POKES56350,0;56358,0;56365,170 56 | #ROOM28,2,16,0,16,16,5(first_landing_item2) 57 | #POPS 58 | 59 | The #POKE(theOfficialSoftwareProjectsPOKEs)(official Software Projects POKE) to 60 | fix this bug is 61 | 62 | POKE #R42183(42183),11 63 | 64 | which moves the item to #R51968(The Hall) (where it is still invisible, but now 65 | reachable). 66 | 67 | [Bug:theInaccessibleItems:The inaccessible items] 68 | The items in #R60160(Conservatory Roof) can only be collected by entering the 69 | room from the ledge on the lower left in #R59904(Under the Roof), but that 70 | ledge is unreachable. 71 | 72 | The #POKE(theOfficialSoftwareProjectsPOKEs)(official Software Projects POKE) to 73 | fix this bug is 74 | 75 | POKE #R56832(56876),4 76 | 77 | which makes it possible to access the right-hand side of #R58368(A bit of tree) 78 | (and from there the lower right side in #R59904(Under the Roof)) from 79 | #R56832(The Banyan Tree). 80 | 81 | [Bug:theUncollectableItem:The uncollectable item] 82 | The item on the far right in #R60160(Conservatory Roof) cannot be collected 83 | because there is an unavoidable nasty in the way. 84 | 85 | The #POKE(theOfficialSoftwareProjectsPOKEs)(official Software Projects POKE) to 86 | fix this bug is 87 | 88 | POKE #R60160#60160(60231),0 89 | 90 | which removes the nasty next to the item. 91 | 92 | [Bug:guardianHalos:Guardian halos] 93 | In a room with a non-black background, a guardian defined with BRIGHT INK is 94 | drawn with a 'halo': a BRIGHT sprite background that contrasts with the 95 | non-BRIGHT room background. There are four guardians affected by this 96 | phenomenon: 97 | 98 | #UDGTABLE(gallery) 99 | { #ROOM9,2,5,7,6,6(halo1) | #ROOM9,2,26,8,6,6(halo2) | 100 | #ROOM15,2,9,8,6,6(halo3) | 101 | #ROOM39,2,6,1,6,6(halo4) } 102 | { =c2 #R51456(On a Branch Over the Drive) | 103 | #R52992(I'm sure I've seen this before..) | 104 | #R59136(Emergency Generator) } 105 | TABLE# 106 | 107 | The following POKEs modify the section of code at #R37334 so that a guardian is 108 | drawn in non-BRIGHT INK and PAPER when the room background is not black, thus 109 | preventing any halo: 110 | 111 |
POKE 37371,40
112 |
POKE 37372,2
113 |
POKE 37373,203
114 |
POKE 37374,177
115 |
POKE 37375,177
116 |
POKE 37376,205
117 |
POKE 37377,201
118 |
POKE 37378,147
119 |
POKE 37379,79
120 | 121 | [Bug:fromTopToBottom:From top to bottom] 122 | In a room that contains a rope and has no exit to the room above, Willy can 123 | jump while at the highest point on the rope (when it's at the leftmost or 124 | rightmost extent of its swing) through the top of the room and reappear on the 125 | floor of the same room. The only rooms where this can happen are #R53760(On the 126 | Roof) and #R64000(The Beach). 127 | 128 | To prevent Willy from climbing high enough up the rope to be able to jump 129 | through the top of the room: 130 | 131 |
POKE #R37781(37782),15
132 |
POKE 37786,15
133 | 134 | The number 15 here determines the highest segment of rope that Willy can reach 135 | (originally 12, three segments higher up). At segment 15, Willy's y-coordinate 136 | never goes below 25 while he's on the rope, and is adjusted down to 24 (see 137 | #R36782) when he jumps off, which is too far from the top of the room (the 138 | maximum height Willy can jump is 20 pixels). At segment 14, Willy's 139 | y-coordinate never goes below 23 while he's on the rope, but is adjusted down 140 | to 16 when he jumps off, which is close enough to the top of the room to be 141 | able to jump through it. 142 | 143 | The code at #R37601 adjusts Willy's location depending on where he's hanging on 144 | to the rope. Specifically, his pixel y-coordinate is set to Y-8, where Y is the 145 | pixel y-coordinate of the segment of rope whose index matches the value of the 146 | rope status indicator at #R34262. 147 | 148 | [Bug:longDistanceNasties:Long distance nasties] 149 | If Willy falls through the bottom of a room and there is a nasty at the top of 150 | the room in a direct line above him, he will die before he reaches the room 151 | below. 152 | 153 | The only room affected by this bug is #R59904(Under the Roof). Use the 154 | following POKE to start the game in that room and see what happens when Willy 155 | steps off the ledge: 156 | 157 | POKE 34795,42 158 | 159 | The problem lies in the section of code at #R38392, which sets the attribute 160 | bytes for the cells occupied by and below Willy's sprite. If Willy is below the 161 | level of the floor, #REGhl (which holds the address of the cell in the 162 | attribute buffer at #R23552) is eventually incremented so that it points to the 163 | cell in the ceiling above Willy in the attribute buffer at #R24064. Then the 164 | routine at #R38430 is called to actually set the attribute byte for the cell, 165 | detects the nasty there, and kills Willy. 166 | 167 |
POKE 38452,208
168 |
POKE 38453,253
169 |
POKE 64976,203
170 |
POKE 64977,76
171 |
POKE 64978,202
172 |
POKE 64979,182
173 |
POKE 64980,144
174 | 175 | [Bug:dontMindYourHead:Don't mind your head] 176 | The code at #R36828 onwards that moves Willy left does not check for a wall 177 | tile at head height, which means Willy can move right to left through a wall 178 | tile at that position. To see this, add a wall tile at head height in 179 | #R57600(The Bathroom) and guide Willy through it: 180 | 181 | POKE 57708,32 182 | 183 | #PUSHS 184 | #POKES57708,32 185 | #WILLY33,17,104,4,15,12,6,4,1(dmyh) 186 | #POPS 187 | 188 | However, the code at #R36942 onwards that moves Willy right does check for a 189 | wall tile at head height (at #R37031), so Willy (correctly) cannot move left to 190 | right through a wall tile at that position. 191 | 192 | Note that the ability to walk from right to left through a wall tile at head 193 | height comes in handy in #R54016(The Forgotten Abbey) - without that ability, 194 | Willy would not be able to collect the item without sacrificing a life. 195 | 196 | [Bug:stuckInTheWall:Stuck in the wall] 197 | If Willy enters #R61696(The Wine Cellar) from the ledge in #R54016(The 198 | Forgotten Abbey), he must keep moving left until he clears all the wall tiles, 199 | otherwise he can become trapped inside them by the conveyor below trying to 200 | move him to the right. 201 | 202 | To get on the conveyor, stand facing left at the top of the ramp in The 203 | Forgotten Abbey (see the first image below) and jump left towards the exit - 204 | Willy will land on the ledge (second image). Then walk left into The Wine 205 | Cellar. 206 | 207 | #UDGTABLE(gallery) { 208 | #WILLY19,2,88,6,0,9,6,6(sitw1_1) | 209 | #WILLY19,1,80,7,0,9,6,6(sitw1_2) | 210 | #WILLY49,29,80,3,26,9,6,6(sitw1_3) 211 | } TABLE# 212 | 213 | Willy can also get stuck in the wall tiles that make up the elephant's head in 214 | #R59392(Dr Jones will never believe this). Start at the location shown in the 215 | first image below, and then walk left. 216 | 217 | #UDGTABLE(gallery) 218 | { #WILLY40,18,24,7,16,1,6,6(sitw2_1) | #WILLY40,15,24,3,13,1,6,6(sitw2_2) } 219 | TABLE# 220 | 221 | Getting stuck in these places is possible because Willy can walk right to left 222 | through wall tiles at head height, but not left to right (see 223 | #BUG(dontMindYourHead)()). 224 | 225 | [Bug:missedNote:The missed note] 226 | Each note of the #R34399(in-game tune) is played twice by the section of code 227 | at #R35644, except the first note when a new game starts. This is 228 | because the in-game music note index at #R34273 is incremented from 0 to 1 on 229 | the first pass through the code before the first note is played. 230 | 231 | [Bug:theBlankConveyor:The blank conveyor] 232 | The conveyor tile in #R60160(Conservatory Roof) is completely blank and its 233 | attribute byte is #b255 - a solid, bright white block - which is characteristic 234 | of unused tiles elsewhere. However, the conveyor tile is used in this 235 | room, and it doesn't look good: 236 | 237 | #UDGTABLE { #ROOM43,2,22,12,10,4(blank_conveyor) } TABLE# 238 | 239 | See also #FACT(unanimatedConveyors)(). 240 | 241 | [Bug:corruptedConveyors:Corrupted conveyors] 242 | The conveyors in #R55296(West of Kitchen), #R56576(The Nightmare Room), 243 | #R61696(The Wine Cellar) and #R62208(Tool#SPACE(2)Shed) don't look quite as 244 | they should. The reason for this is a bug in the code at #R36177: if a graphic 245 | data byte of the background, floor, wall, nasty or ramp tile in a room matches 246 | the attribute byte of the conveyor tile to be drawn, the '#S/CPIR/' instruction 247 | exits early, leaving #REGhl pointing at the wrong location. 248 | 249 | Here are some 'before' and 'after' pictures showing what the conveyors actually 250 | look like and what they look like when drawn properly. 251 | 252 | #UDGTABLE(gallery) 253 | { #R55296(West of Kitchen) | 254 | #ROOM24,2,21,11,7,3,2(conveyor24_before) | 255 | #ROOM24,2,21,11,7,3,14(conveyor24_after) } 256 | { #R56576(The Nightmare Room) | 257 | #ROOM29,2,26,6,3,3,0(conveyor29_before) | 258 | #ROOM29,2,26,6,3,3,8(conveyor29_after) } 259 | { #R61696(The Wine Cellar) | 260 | #ROOM49,2,26,11,7,3,2(conveyor49_before) | 261 | #ROOM49,2,26,11,7,3,8(conveyor49_after) } 262 | { #R62208(Tool#SPACE(2)Shed) | 263 | #ROOM51,2,6,14,20,2,2(conveyor51_before) | 264 | #ROOM51,2,6,14,20,2,10(conveyor51_after) } 265 | { =t | Before | After } 266 | TABLE# 267 | 268 | To fix the bug: 269 | 270 |
POKE 36178,151
271 |
POKE 36181,9
272 |
POKE 36183,205
273 |
POKE 36184,38
274 |
POKE 36185,255
275 |
POKE 65318,9
276 |
POKE 65319,190
277 |
POKE 65322,75
278 |
POKE 65323,35
279 |
POKE 65324,201
280 | 281 | See also #BUG(corruptedNasties)(). 282 | 283 | [Bug:veryCorruptedConveyor:Very corrupted conveyor] 284 | You might be thinking that the fix given above for the corrupted conveyors bug 285 | does not do much for the appearance of the conveyor in #R56576(The Nightmare 286 | Room). Perhaps that's because the real problem with this conveyor is that its 287 | attribute byte and graphic data - which should occupy addresses 288 | #R56781(#N56781-#N56789) - appear to have been shifted by one byte back to 289 | #N56780-#N56788 (overwriting the eighth graphic byte of the ramp tile). If they 290 | are shifted along to the right spot, the conveyor takes on a much more 291 | reasonable appearance: 292 | 293 | #UDGTABLE(gallery) 294 | { #ROOM29,2,26,6,3,3,0(conveyor29_before) | 295 | #PUSHS #FOR(56789,56781,-1)||n|#POKES(n,#PEEK(n-1))|| 296 | #ROOM29,2,26,6,3,3,14(conveyor29_after2) 297 | #POPS } 298 | { Before | After } 299 | TABLE# 300 | 301 | In addition, if the byte at #N56789 (#b85) is shifted back round to #N(56780), 302 | it seems to fix the appearance of the ramp tile (by filling the gap at the 303 | bottom): 304 | 305 | #UDGTABLE(gallery) 306 | { #ROOM29,2,6,12,5,4,0(ramp29_before) | 307 | #PUSHS #POKES56780,85 308 | #ROOM29,2,6,12,5,4,8(ramp29_after) 309 | #POPS } 310 | { Before | After } 311 | TABLE# 312 | 313 | [Bug:corruptedNasties:Corrupted nasties] 314 | The nasty tiles down the right hand side of #R58368(A bit of tree) don't look 315 | quite as they should. The reason for this is the same bug in the code at 316 | #R36177 that causes the #BUG(corruptedConveyors)(corrupted conveyors): if a 317 | graphic data byte of the background, floor or wall tile in a room matches the 318 | attribute byte of the nasty tile to be drawn, the '#S/CPIR/' instruction exits 319 | early, leaving #REGhl pointing at the wrong location. 320 | 321 | Here are the 'before' and 'after' pictures showing what the nasties actually 322 | look like and what they look like when drawn properly. 323 | 324 | #UDGTABLE(gallery) 325 | { #ROOM36,2,28,0,4,4,0(nasty36_before) | 326 | #ROOM36,2,28,0,4,4,8(nasty36_after) } 327 | { Before | After } 328 | TABLE# 329 | 330 | [Bug:invalidGridLocation:Invalid grid location] 331 | If the LSB of the system variable FRAMES is #b142 when the game starts, the 332 | routine at #R34499 will ask for the code at the grid location 'D>', which 333 | doesn't exist on the code sheet. 334 | 335 | Should you ever need it, the code for 'D>' is 1444. 336 | 337 | POKE #R34555(34556),180 338 | 339 | [Bug:thePauseBug:The pause bug] 340 | The Spectrum will hang when trying to resume the game after being paused if an 341 | Interface 1 is connected. This happens because the #REGc register holds #b0 342 | instead of #b254 when the keyboard is read by the '#S/IN A,(C)/' instruction at 343 | #R35620. 344 | 345 | POKE #R35615(35615),62: POKE #R35620(35620),219: POKE #R35620(35621),254 346 | 347 | [Bug:theAtticBug:The Attic bug] 348 | Perhaps the most famous of all the bugs in Jet Set Willy, the Attic bug is 349 | responsible for certain rooms becoming impossible to navigate after Willy has 350 | visited #R59648(The Attic), thus making it impossible to complete the game. 351 | 352 | The root cause of the bug is a misplaced arrow, defined by the entity 353 | specification at #R59900. The second byte of the entity specification is an 354 | index into the table of screen buffer addresses at #R33280, and determines the 355 | arrow's y-coordinate; the index should be an even number, but the value of the 356 | second byte is #b213, which means the arrow-drawing routine at #R37310 looks up 357 | the word at #N33493 and finds #N(41066), way outside the actual screen buffer 358 | at #N24576-#N28671. As a result, the misplaced arrow is 'drawn' at the 359 | following addresses: 360 | 361 | #LIST 362 | { #R40704(#N40810-#N40841): unused (set to #b65, with no effect on the game) } 363 | { #R41064(#N41066-#N41097): the third byte of entity definition #b13 through to 364 | the second byte of entity definition #b17 (set to #b255); this affects the 365 | guardians in #R52736(Rescue Esmerelda), #R52992(I'm sure I've seen this 366 | before..), #R53248(We must perform a Quirkafleeg), #R49408(The Bridge), 367 | #R50688(Entrance to Hades), #R56064(The Chapel) and #R58880(Priests' Hole) } 368 | { #R41320(#N41322-#N41353): the third byte of entity definition #b45 through to 369 | the second byte of entity definition #b49 (set to #b65); this affects the 370 | guardians in #R52480(Out on a limb), #R55808(East Wall Base), #R52224(Tree 371 | Top), #R51200(Inside the MegaTrunk), #R55040(The Kitchen) and #R55296(West of 372 | Kitchen) } 373 | LIST# 374 | 375 | The #POKE(theOfficialSoftwareProjectsPOKEs)(official Software Projects POKE) to 376 | fix the Attic bug is 377 | 378 | POKE #R59900(59901),82 379 | 380 | which sets the pixel y-coordinate of the arrow to 41. 381 | -------------------------------------------------------------------------------- /sources/facts.ref: -------------------------------------------------------------------------------- 1 | ; Copyright 2012, 2014-2021, 2025 Richard Dymond (rjdymond@gmail.com) 2 | 3 | [Fact:gameOverAt1am:Game over at 1am] 4 | The game ends abruptly at 1am (see #R35442), even if Willy has collected all 5 | the items, or has reached his bed and is already on his way to the toilet, or 6 | already has his head down the toilet. 7 | 8 | This fact seems to contradict the message that scrolls across the lower part of 9 | the title screen (see #R33876), which implies that Willy must collect all the 10 | items before midnight. 11 | 12 | [Fact:tunes:Tunes] 13 | The tune played on the title screen is Beethoven's Moonlight Sonata. The 14 | tune played during gameplay is If I Were a Rich Man from the musical 15 | Fiddler on the Roof. 16 | 17 | [Fact:theMovingBed:The moving bed] 18 | The bed in #R58112(Master Bedroom) is actually a conveyor moving left to right, 19 | but it doesn't appear to move because the first and third bytes of the conveyor 20 | tile (shown below) are #b85 (01010101), which remains #b85 after being rotated 21 | left or right twice. 22 | 23 | #UDGTABLE 24 | { #UDG58318,41(moving_bed) } 25 | TABLE# 26 | 27 | See also #FACT(unanimatedConveyors)(). 28 | 29 | [Fact:theMusicOfLife:The music of life] 30 | When Willy loses a life, the pitch of the notes in the in-game music decreases, 31 | and their length increases; see #R35668. 32 | 33 | To prevent this from happening: 34 | 35 | POKE 35674,126 36 | 37 | [Fact:takingABreak:Taking a BREAK] 38 | The game quits if BREAK - that is, CAPS SHIFT and SPACE - is pressed (see 39 | #R35505). This works at any time, including when Willy is running to the toilet 40 | or already has his head down it. 41 | 42 | [Fact:writetyper:WRITETYPER] 43 | If you take Willy to the floor at the bottom of the staircase in #R56320(First 44 | Landing) and type the word WRITETYPER, a cheat mode is activated that enables 45 | Willy to teleport to any room (see #R35743). 46 | 47 | For example, to teleport to #R58112(Master Bedroom), hold down keys '1', '2' 48 | and '6', and then press '9' to activate the teleporter. For a list of all the 49 | rooms and their teleport codes, see the #LINK(Rooms)(Rooms page). 50 | 51 | [Fact:asYouWere:As you were] 52 | Willy's animation frame at #R34258 and direction flag at #R34256 are not 53 | initialised before the game starts, so his animation frame and the direction 54 | he's facing at the start of a game will be whatever they were when he died in 55 | the previous game. (The first time the game starts, Willy's animation frame is 56 | 0 and he's facing right.) 57 | 58 | [Fact:whiteSeekingMissile:White-seeking missile] 59 | The routine at #R37310, when drawing an arrow, kills Willy if the arrow hits 60 | anything with white INK; this means, for example, that Willy would be killed if 61 | the arrow hits a white rope. It is no coincidence, then, that in the rooms that 62 | have both a rope and an arrow (#R53248(We must perform a Quirkafleeg), 63 | #R53760(On the Roof) and #R64000(The Beach)), the rope is not white. 64 | 65 | To see Willy die when the arrow hits the rope in #R64000(The Beach): 66 | 67 |
POKE #R34794(34795),58 (start in #R64000(The Beach))
68 |
POKE #R64160(64160),15 (set the INK of the room's background tile to 69 | white)
70 |
POKE #R34789(34790),128 (set Willy's initial pixel y-coordinate to 64) 71 |
72 |
POKE #R34799(34800),2: POKE #R34799(34801),93 (set Willy's initial 73 | attribute buffer location)
74 | 75 | [Fact:beerInTheWall:Beer in the wall?] 76 | The wall tile in #R62976(West Wing) is very similar to the beer mug item in 77 | #R56576(The Nightmare Room); they differ only in the bottom row of pixels: 78 | 79 | #UDGTABLE(gallery) 80 | { #R62976(West Wing) wall tile | #UDG63155,39(wall54) } 81 | { #R56576(The Nightmare Room) item | #UDG56801,3(item29) } 82 | TABLE# 83 | 84 | [Fact:dangerousConnections:Dangerous connections] 85 | There are several places in Willy's mansion where it is obvious that he is in 86 | danger of falling from a great height and entering an infinite death loop. 87 | However, there are also some places where it is far from obvious that merely 88 | moving from one room to another will lead to an infinite death loop. 89 | 90 | For example, the room below #R50432(The Security Guard) is #R50688(Entrance to 91 | Hades), from which there is no escape. 92 | 93 | For another example, the room above #R59392(Dr Jones will never believe this) 94 | is #R53248(We must perform a Quirkafleeg), where the long row of nasties at the 95 | bottom of the pit ensures certain (repeated) death. 96 | 97 | [Fact:unusedRoom:Unused room] 98 | There are 61 room definitions occupying pages #b192-#b252, but the definition 99 | for room #b47 in #R61184(page #b239) is not used. Room #b47 is completely empty 100 | and can only be accessed by activating #FACT(writetyper)(cheat mode) and using 101 | the teleport code '123469'. 102 | 103 | However, the (inaccessible) right exit from #R60928(Tree Root) is set to 104 | #b47 (see #R61161(#N61162)), which suggests that there was an intention to 105 | place room #b47 there at some point during the game's development. 106 | 107 | [Fact:unusedGridLocation:Unused grid location] 108 | The routine at #R34499, which is responsible for randomly selecting one of the 109 | 180 grid locations on the code sheet by its index (#b0-#b179), never selects 110 | index #b179 (R9); as a result, the code at grid location R9 is never asked for, 111 | even though it's present on the code sheet (it's 2423). 112 | 113 | To give the code at grid location R9 a chance of being asked for: 114 | 115 | POKE #R34555(34556),180 116 | 117 | [Fact:unusedItems:Unused items] 118 | There are five rooms in which nontrivial item graphics are defined, but not 119 | used. 120 | 121 | #UDGTABLE(gallery) 122 | { #R54497(Ballroom East) | #UDG54497,3(item20) } 123 | { #R55265(The Kitchen) | #UDG55265,3(item23) } 124 | { #R59873(The Attic) | #UDG59873,3(item41) } 125 | { #R60897(Under the Drive) | #UDG60897,3(item45) } 126 | { #R61665(Nomen Luni) | #UDG61665,3(item48) } 127 | TABLE# 128 | 129 | [Fact:unusedNasties:Unused nasties] 130 | There are seven rooms in which nontrivial nasty graphics are defined, but not 131 | used. 132 | 133 | #UDGTABLE(gallery) 134 | { #R50363(The Drive) | #UDG50364,68(nasty04) } 135 | { #R54715(Ballroom West) | #UDG54716,66(nasty21) } 136 | { #R56251(The Chapel) | #UDG56252,66(nasty27) } 137 | { #R56763(The Nightmare Room) | #UDG56764,69(nasty29) } 138 | { #R57019(The Banyan Tree) | #UDG57020,14(nasty30) } 139 | { #R58811(Orangery) | #UDG58812,6(nasty37) } 140 | { #R61627(Nomen Luni) | #UDG61628,6(nasty48) } 141 | TABLE# 142 | 143 | [Fact:unusedFloor:Unused floor] 144 | There is one room in which a nontrivial floor graphic is defined, but not used. 145 | 146 | #UDGTABLE(gallery) 147 | { #R52137(The Hall) | #UDG52138,71(floor11) } 148 | TABLE# 149 | 150 | [Fact:unusedRamp:Unused ramp] 151 | There is one room in which a nontrivial ramp graphic is defined, but not used. 152 | 153 | #UDGTABLE(gallery) 154 | { #R50884(Entrance to Hades) | #UDG50885,7(ramp06) } 155 | TABLE# 156 | 157 | Though unused, the ramp has its location set to (14,7); perhaps it was used at 158 | some point during development. 159 | 160 | [Fact:unusedConveyor:Unused conveyor] 161 | There is one room in which a nontrivial conveyor graphic is defined, but not 162 | used. 163 | 164 | #UDGTABLE(gallery) 165 | { #R56269(The Chapel) | #UDG56270,7(conveyor27) } 166 | TABLE# 167 | 168 | [Fact:unusedGuardian:Unused guardian] 169 | The guardian whose graphic data lives at #R45312 is unused. 170 | 171 | #UDGTABLE { 172 | #UDGARRAY2,7,,2(45440-45457-1-16;45312-45329-1-16)(guardian177-0) | 173 | #UDGARRAY2,7,,2(45472-45489-1-16;45344-45361-1-16)(guardian177-1) | 174 | #UDGARRAY2,7,,2(45504-45521-1-16;45376-45393-1-16)(guardian177-2) | 175 | #UDGARRAY2,7,,2(45536-45553-1-16;45408-45425-1-16)(guardian177-3) 176 | } TABLE# 177 | 178 | However, this guardian made an appearance later on in the room 'Macaroni Ted' 179 | in Jet Set Willy II. 180 | 181 | [Fact:uniqueGuardians:Unique guardians] 182 | There are seven rooms that contain a unique guardian (that is, a guardian that 183 | does not appear in any other room). 184 | 185 | #UDGTABLE(gallery) 186 | { #R49152(The Off Licence) | 187 | #UDGARRAY2,69,,2(48768-48785-1-16)(guardian190-4-69) } 188 | { #R49408(The Bridge) | 189 | #UDGARRAY2,68,,2(47488-47505-1-16)(guardian185-4-68) } 190 | { #R52736(Rescue Esmerelda) | 191 | #UDGARRAY2,23,,2(45184-45201-1-16)(guardian176-4-23) } 192 | { #R55552(Cold Store) | 193 | #UDGARRAY2,14,,2(48384-48401-1-16)(guardian189-0-14) } 194 | { #R56576(The Nightmare Room) | 195 | #UDGARRAY2,6,,2(40000-40017-1-16)(foot) } 196 | { #R59648(The Attic) | 197 | #UDGARRAY2,3,,2(45056-45073-1-16)(guardian176-0-3) 198 | #UDGARRAY2,5,,2(45120-45137-1-16)(guardian176-2-5) } 199 | { #R60672(Under the Drive) | 200 | #UDGARRAY2,3,,2(44672-44689-1-16)(guardian174-4-3) } 201 | TABLE# 202 | 203 | [Fact:commonestGuardian:Commonest guardian] 204 | The commonest guardian is the bird, whose graphic data lives at #R48128. 205 | 206 | #UDGTABLE { 207 | #UDGARRAY2,66,,2(48128-48145-1-16)(guardian188-0-66) | 208 | #UDGARRAY2,66,,2(48160-48177-1-16)(guardian188-1-66) | 209 | #UDGARRAY2,66,,2(48192-48209-1-16)(guardian188-2-66) | 210 | #UDGARRAY2,66,,2(48224-48241-1-16)(guardian188-3-66) | 211 | #UDGARRAY2,66,,2(48256-48273-1-16)(guardian188-4-66) | 212 | #UDGARRAY2,66,,2(48288-48305-1-16)(guardian188-5-66) | 213 | #UDGARRAY2,66,,2(48320-48337-1-16)(guardian188-6-66) | 214 | #UDGARRAY2,66,,2(48352-48369-1-16)(guardian188-7-66) 215 | } TABLE# 216 | 217 | There are 23 instances of this guardian spread over 18 rooms: four in 218 | #R50176(The Drive), two in each of #R49408(The Bridge) and #R51200(Inside the 219 | MegaTrunk), and one in each of #R51968(The Hall), #R52224(Tree Top), 220 | #R52480(Out on a limb), #R52736(Rescue Esmerelda), #R52992(I'm sure I've seen 221 | this before..), #R53248(We must perform a Quirkafleeg), #R54272(Ballroom East), 222 | #R54784(To the Kitchens Main Stairway), #R58368(A bit of tree), 223 | #R58880(Priests' Hole), #R60672(Under the Drive), #R61440(Nomen Luni), 224 | #R61952(Watch Tower), #R62976(West Wing) and #R63488(West Wing Roof). 225 | 226 | [Fact:commonestItem:Commonest item] 227 | The commonest item is the bell, which appears in #R53504(Up on the 228 | Battlements), #R53248(We must perform a Quirkafleeg) and #R52992(I'm sure I've 229 | seen this before..). 230 | 231 | #UDGTABLE { #UDG53217,11(item15) } TABLE# 232 | 233 | Every other item is unique to its own room. 234 | 235 | [Fact:commonestCodes:Commonest codes] 236 | The commonest codes on the code sheet are 2411 (at H7, O7, R7 and C9) and 3443 237 | (at O3, D6, L8 and R8). 238 | 239 | [Fact:codesFromCode:Codes from code] 240 | The codes for grid locations A0-Q9 are derived not from a deliberately crafted 241 | data table but from the unused code remnants at #R40448. 242 | 243 | [Fact:oneWaySaw:The one-way saw] 244 | The saw guardian in #R50944(Cuckoo's Nest) always has its blade facing left, 245 | even when it's moving left to right (see #R41848). The reason for this is that 246 | its base sprite is 0 and its animation frame mask is 011, which means it cycles 247 | through sprites 0, 1, 2 and 3 only (see #R37393). 248 | 249 | #UDGTABLE 250 | { #GUARDIAN181,0,3 | #GUARDIAN181,1,3 | #GUARDIAN181,2,3 | #GUARDIAN181,3,3 } 251 | TABLE# 252 | 253 | Use the following POKE to make the saw face right when moving in that 254 | direction: 255 | 256 | POKE #R41848(41849),227 257 | 258 | All the other saw guardians in the game have the blade facing right when moving 259 | in that direction, because their animation frame mask is 111 (see #R41376, 260 | #R41384 and #R41664). 261 | 262 | [Fact:disrespectfulMonk:The disrespectful monk] 263 | Like the one-way saw in Cuckoo's Nest, the monk in 264 | #R56064(The Chapel) always faces left, even when he's moving left to right. 265 | Also like the one-way saw, the reason for this is that his base sprite is 0 and 266 | his animation frame mask is 011, which means he cycles through sprites 0, 1, 2 267 | and 3 only. 268 | 269 | #UDGTABLE 270 | { #GUARDIAN180,0,3 | #GUARDIAN180,1,3 | #GUARDIAN180,2,3 | #GUARDIAN180,3,3 } 271 | TABLE# 272 | 273 | Use the following POKE to make the monk face right when walking in that 274 | direction: 275 | 276 | POKE #R41784(41785),227 277 | 278 | The saw in Cuckoo's Nest and the monk in The Chapel are the only horizontal 279 | guardians that use only 4 out of 8 possible animation frames in a particular 280 | room. 281 | 282 | [Fact:someAirLeft:Some air left?] 283 | The message at #R33873 ('AIR') may be unused, but it is still a poignant 284 | reminder of Willy's former career as a miner. 285 | 286 | [Fact:mariasDodgyDepthPerception:Maria's dodgy depth perception] 287 | Whether Maria raises her arm at Willy in #R58112(Master Bedroom) depends on his 288 | height above floor level, as opposed to his distance away from her (see the 289 | code at #R38224); this means that she raises her arm not only as Willy walks up 290 | the ramp towards her, but also if he jumps in the air at the entrance to the 291 | room. 292 | 293 | [Fact:ropeClimbingForBeginners:Rope climbing for beginners] 294 | In order to climb down a rope, Willy must move in the same direction as the 295 | rope is swinging (left if the rope is swinging right to left, right if the rope 296 | is swinging left to right); to climb up a rope, he must move in the opposite 297 | direction (see #R37756). 298 | 299 | [Fact:theEncroachingRope:The encroaching rope] 300 | In the entity buffer at #R33024, the definition for a rope uses the second and 301 | fourth bytes of the following definition (in addition to the eight bytes of its 302 | own slot). This means that to avoid corrupting another entity, the rope must be 303 | followed in the room's list of entity definitions either by an arrow (which 304 | doesn't use the second and fourth bytes of its slot), or by nothing. 305 | 306 | Note, however, that if a rope were the eighth entity specified in a room, byte 307 | 11 of its buffer would occupy the third byte in the otherwise unused area at 308 | #R33089, and would not be reinitialised by the routine at #R35090 after Willy 309 | loses a life or re-enters the room. This means, for example, that if Willy dies 310 | while on the rope, bit 0 of byte 11 (which indicates whether he is on the rope) 311 | remains set; then, if Willy starts moving in the same direction as the rope is 312 | swinging, he will teleport into the room above. This happens because: 313 | 314 | #LIST 315 | { The section of code at #R37726 erroneously assumes that Willy is on the rope, 316 | and updates the rope status indicator at #R34262 from 0 (Willy is not on the 317 | rope) to 1 (Willy is on the rope, with the centre of his sprite anchored at 318 | segment 1). } 319 | { On the next pass through the main loop, the section of code at #R37558 320 | erroneously detects Willy on the rope at segment 1, and accordingly sets his 321 | y-coordinate at #R34255 to 246; this is the y-coordinate of segment 1 of the 322 | rope (6) minus 16. } 323 | { On the next pass through the main loop, the code at #R35281 picks up Willy's 324 | y-coordinate (now 246) from #R34255, finds that it is 225 or greater, and 325 | moves Willy into the room above. } 326 | LIST# 327 | 328 | [Fact:ropesBeforeArrows:Ropes before arrows] 329 | The rope-drawing code at #R37540 places Willy on the rope if it is touching 330 | anything else (Willy or otherwise) that's already been drawn. This means, for 331 | example, that if an arrow is drawn before the rope (by appearing before it in 332 | the room's entity specifications), Willy will be immediately transported onto 333 | the rope when the arrow hits it. 334 | 335 | To see this happen in #R64000(The Beach): 336 | 337 |
POKE #R34794(34795),58 (start in #R64000(The Beach))
338 |
POKE #R64240(64240),60: POKE #R64240(64241),84 (entity specification for 339 | the arrow)
340 |
POKE #R64242(64242),1: POKE #R64242(64243),14 (entity specification for 341 | the rope)
342 |
POKE #R34789(34790),128 (set Willy's initial pixel y-coordinate to 64) 343 |
344 |
POKE #R34799(34800),2: POKE #R34799(34801),93 (set Willy's initial 345 | attribute buffer location)
346 | 347 | [Fact:flickeringClock:The flickering clock] 348 | When a room is initialised - upon Willy dying or entering a room - the item 349 | count and clock are briefly reset to '000' and '00:00 m' respectively by 350 | the routine at #R35090 before being restored by the section of code at #R35377 351 | in the main loop. 352 | 353 | [Fact:quadridirectionalGuardian:The quadridirectional guardian] 354 | The cyan guardian in #R51456(On a Branch Over the Drive) moves up and down (not 355 | very gracefully), while its red and green cousins in #R54784(To the 356 | Kitchens#SPACE(4)Main Stairway) and #R57600(The Bathroom) move left and right. 357 | 358 | #UDGTABLE { 359 | #GUARDIAN185,0,13 | #GUARDIAN185,1,13 | #GUARDIAN185,2,13 | #GUARDIAN185,3,13 360 | } TABLE# 361 | 362 | It is the only type of guardian that can be seen moving vertically in one room 363 | and horizontally in another. 364 | 365 | [Fact:doubleDescent:Double descent] 366 | There is a maximum distance that Willy can fall before the landing will kill 367 | him, but it's possible for Willy to fall a greater distance than that if his 368 | descent is punctuated by a transition from one room into the room below; if 369 | each portion of his descent on either side of the transition is less than the 370 | maximum distance, he will land safely. 371 | 372 | To demonstrate this, jump left near the bottom of the ramp in 373 | #R60160(Conservatory Roof) - Willy will land safely on the ramp in the room 374 | below (#R58624(Orangery)). But if Willy jumps left off the ramp in Orangery, 375 | he will die when he lands on a lower part of the ramp. 376 | 377 | The code responsible for this is at #R38115 - it resets the airborne status 378 | indicator at #R34257 to 2 if it's currently 10 or less. 379 | 380 | [Fact:highJump:High jump] 381 | At certain points on a ramp, Willy can jump higher than the normal 20 pixels 382 | above his starting point. Depending on where he is standing on the ramp, he 383 | will jump 20, 22, 24 or 26 pixels. 384 | 385 | #UDGTABLE(gallery) 386 | { #WILLY28,8,102,0,6,10,6,6(high_jump0) | 387 | #WILLY28,8,100,1,6,10,6,6(high_jump1) | 388 | #WILLY28,8,98,2,6,10,6,6(high_jump2) | 389 | #WILLY28,8,96,3,6,10,6,6(high_jump3) } 390 | { #WILLY21,3,94,7,1,9,6,6(high_jump7) | 391 | #WILLY21,3,92,6,1,9,6,6(high_jump6) | 392 | #WILLY21,3,90,5,1,9,6,6(high_jump5) | 393 | #WILLY21,3,88,4,1,9,6,6(high_jump4) } 394 | { 26 pixels | 24 pixels | 22 pixels | 20 pixels } 395 | TABLE# 396 | 397 | When the jump key is pressed while Willy is standing on a ramp, he is drawn at 398 | the pixel y-coordinate Y, where Y is his original pixel y-coordinate rounded 399 | down to the nearest multiple of 8 (see #R38344, which ignores the ramp below 400 | Willy when the airborne status indicator at #R34257 is 1, and enters #R38455 401 | with #REGb=0). So before the jumping animation even starts, Willy is already up 402 | to 6 pixels above the ramp itself. 403 | 404 | Also, during a jump Willy rises and then falls until his pixel y-coordinate is 405 | Y again, and if the jump key is still being pressed, he will jump from that 406 | point instead of falling all the way back to the ramp first. 407 | 408 | [Fact:guardianColours:Guardian colours] 409 | The most common guardian colour is yellow (25 instances), and the least common 410 | colour is blue (2 instances, both BRIGHT, in #R54016(The Forgotten Abbey) and 411 | #R59648(The Attic)). 412 | 413 | As for the other colours, there are 18 red guardians, 20 magenta guardians, 19 414 | green guardians, 18 cyan guardians, and 7 white guardians; there are no black 415 | guardians anywhere. 416 | 417 | [Fact:guardiansNeedAClearPath:Guardians need a clear path] 418 | The guardian-drawing code at #R37334 kills Willy if a guardian collides with 419 | anything that's already been drawn in the room. This means, for example, that 420 | Willy is killed if a guardian hits a nasty, a wall or the floor. 421 | 422 | See this happen by placing a wall block in the path of the guardian in 423 | #R57600(The Bathroom): 424 | 425 | POKE #R57600#57600(57635),128 426 | 427 | In addition, if an arrow appears before a guardian in a room's list of entity 428 | specifications, Willy will be killed if the arrow hits the guardian. See this 429 | happen by replacing the first guardian in #R57856(Top Landing) with an arrow: 430 | 431 |
POKE #R58096(58096),#R41440(60)
432 |
POKE 58097,56
433 | 434 | [Fact:guardianOrFixture:Guardian or fixture?] 435 | Entity #b89, defined at #R41672, and appearing in #R51968(The Hall) and 436 | #R62976(West Wing), is the only guardian that does not move up, down, left or 437 | right. 438 | 439 | #UDGTABLE { #FRAMES(guardian176-6-6;guardian176-7-6)(entity89) } TABLE# 440 | 441 | However, the minimum and maximum pixel y-coordinates for this guardian are 442 | defined as 40 and 56 respectively, which suggests that it did move up and down 443 | at some point during the development of the game. 444 | 445 | [Fact:unanimatedConveyors:Unanimated conveyors] 446 | In addition to the bed in #R58112(Master Bedroom) (see #FACT(theMovingBed)()) 447 | and the white ledge in #R60160(Conservatory Roof) (see 448 | #BUG(theBlankConveyor)()), there are three other unanimated conveyors in the 449 | game: 450 | 451 | #UDGTABLE(gallery) 452 | { #ROOM21,2,15,10,14,6(unanimated_conveyor1) | 453 | #ROOM39,2,8,8,16,6(unanimated_conveyor2) | 454 | #ROOM60,2,16,10,16,6(unanimated_conveyor3) } 455 | { The table in #R54528(Ballroom West) | 456 | #R59136(Emergency Generator) | 457 | The lower deck of the yacht in #R64512(The Bow) } 458 | TABLE# 459 | 460 | Note also that if it weren't for a bug in how conveyors are drawn (see 461 | #BUG(corruptedConveyors)()), the conveyor in #R61696(The Wine Cellar) would be 462 | unanimated as well. 463 | 464 | [Fact:unanimatedGuardians:Unanimated guardians] 465 | There are a few vertical guardians that have only one animation frame, and so 466 | are in a sense unanimated (though they do still move up and down). 467 | 468 | #UDGTABLE(gallery) 469 | { #GUARDIAN156,3,66 | 470 | #GUARDIAN186,2,5 | 471 | #GUARDIAN156,2,6 #GUARDIAN156,4,6 } 472 | {
#R54272(Ballroom East)
#R57856(Top Landing)
| 473 | #R52480(Out on a limb) | 474 | #R56576(The Nightmare Room) } 475 | TABLE# 476 | 477 | In contrast, every horizontal guardian has either 4 or 8 animation frames. 478 | 479 | [Fact:underanimatedGuardians:Underanimated guardians] 480 | Though the game engine can manage a vertical guardian with eight frames of 481 | animation, there are no such guardians in the game: no vertical guardian uses 482 | more than four animation frames. This means that in byte 0 of a vertical 483 | guardian's buffer - bits 5-7 of which hold the animation frame index - bit 7 is 484 | always unused in practice. 485 | 486 | [Fact:unusedEntityDefinition:Unused entity definition] 487 | Entity definition #b43 at #R41304 contains data but is not used. 488 | 489 | Which room might this vertical guardian have inhabited at some point while the 490 | game was being developed? We'll probably never know. 491 | 492 | [Fact:untouchableEntityDefinition:Untouchable entity definition] 493 | Entity definition #b127 at #R41976 should not be modified or used to define an 494 | actual entity, because its first byte (#b255) is used to terminate the entity 495 | buffer at #R33024. 496 | 497 | [Fact:rampsVsWalls:Ramps v. walls] 498 | Wall tiles usually block Willy's path, but if he's on a ramp, he can sometimes 499 | walk straight through them. This can be seen when Willy walks down the ramp in 500 | #R60416(On top of the house). 501 | 502 | #WILLY44,11,70,4,8,7,8,6(rvw1) 503 | 504 | Willy can also walk through a wall tile while going up a ramp. There are no 505 | opportunities for him to perform this feat in the original game, but the 506 | following POKE adds a wall tile near the ramp in #R57600(The Bathroom) to 507 | illustrate the phenomenon: 508 | 509 | POKE 57682,128 510 | 511 | #PUSHS 512 | #POKES57682,128 513 | #WILLY33,8,84,1,5,9,8,6,1(rvw2) 514 | #POPS 515 | 516 | [Fact:oneItemBehindAnother:One item behind another] 517 | There are two items in #R64000(The Beach), but they are both at the same 518 | location, so it looks as if there is only one. 519 | 520 | To move one of the items two cells to the left: 521 | 522 | POKE #R42438(42438),180 523 | 524 | [Fact:itemsAndWhiteness:Items and whiteness] 525 | If an item lies in the path of an arrow or white guardian, it will be collected 526 | when the arrow or guardian hits it. This is because the routine at #R37841 527 | collects an item if the cell it occupies has white INK. 528 | 529 | To replace the guardian in #R57600(The Bathroom) with an arrow and watch it 530 | collect the item (make sure Willy jumps as the arrow approaches): 531 | 532 |
POKE #R57840(57840),60
533 |
POKE 57841,214
534 | 535 | To make the guardian in #R57600(The Bathroom) white and place the item in its 536 | path: 537 | 538 |
POKE #R41432(41433),103
539 |
POKE #R42239(42239),33
540 |
POKE #R42495(42495),135
541 | 542 | In addition, any item situated in a room that has a background with white INK 543 | will be automatically collected as soon as Willy enters the room (see 544 | #BUG(selfCollectingItem)()). 545 | 546 | [Fact:uneconomicalUnderworld:The uneconomical underworld] 547 | The demonic face guardian (as seen in #R50688(Entrance to Hades), #R56064(The 548 | Chapel) and #R58880(Priests' Hole)) uses three entity definitions 549 | (#R41088(#b16), #R41096(#b17) and #R41104(#b18)), but it could be adjusted to 550 | use only two, where the upper-left and upper-right portions share an entity 551 | definition with a different base sprite and x-coordinate. 552 | 553 | #UDGTABLE(gallery) 554 | { #GUARDIAN186,4,4 | #GUARDIAN186,5,4 } 555 | { =c2 #GUARDIAN186,6,4 } 556 | TABLE# 557 | 558 | Use the following POKEs to free up entity definition #b16: 559 | 560 |
POKE #R50928(50928),17
561 |
POKE #R56304(56304),17
562 |
POKE #R59120(59120),17
563 | 564 | [Fact:slipperySlopes:Slippery slopes] 565 | In #R56064(The Chapel) and #R57344(Halfway up the East Wall), the ramps act as 566 | conveyors that push Willy down the slope, because the ramp tile has the same 567 | attribute byte as the conveyor tile (which is not actually used in either 568 | room). There are no conveyor-ramps anywhere else in the game. 569 | 570 | [Fact:floorRamps:Floor ramps] 571 | There are five rooms in which the floor tiles behave like ramps because their 572 | attribute byte matches that of the (unused) ramp tile: 573 | 574 | #LIST 575 | { #R52480(Out on a limb) (25 tiles) } 576 | { #R52736(Rescue Esmerelda) (13 tiles) } 577 | { #R52992(I'm sure I've seen this before..) (4 tiles) } 578 | { #R53504(Up on the Battlements) (4 tiles) } 579 | { #R53760(On the Roof) (5 tiles) } 580 | LIST# 581 | 582 | [Fact:toPauseOrNotToPause:To pause or not to pause] 583 | If the in-game music has been switched off, the game will pause automatically 584 | after a period of inactivity; however, if the in-game music is playing, it will 585 | not (see #R35644). 586 | 587 | [Fact:cheatsNeverCycleColours:Cheats never cycle colours] 588 | While the game is paused, the INK and PAPER colours are cycled at regular 589 | intervals, unless the #FACT(writetyper)(WRITETYPER cheat mode) has been 590 | activated (see #R35553). 591 | 592 | [Fact:nomenLuni:Nomen Luni?] 593 | The name of the room #R61440(Nomen Luni) is a pun on 'Nomen Ludi', a Latin 594 | translation of 'The Name of the Game', which appeared in adverts for the 595 | aeroplane shoot-em-up game 596 | Zzoom by 597 | Imagine Software Ltd. 598 | 599 | In #R61440(Nomen Luni) and the room below (#R59904(Under the Roof)) you can see 600 | an aeroplane that has crashed into the roof of Willy's mansion: 601 | 602 | #UDGTABLE { 603 | #RFRAME48 604 | #RFRAME42 605 | #UDGARRAY32,,1(0x1024)(*aeroplane) 606 | #OVER(0,0,,,3)($f)($f)(aeroplane,48) 607 | #OVER(0,16,,,3)($f)($f)(aeroplane,42) 608 | #FRAMES(aeroplane)({ScreenshotImagePath}/aeroplane) 609 | } TABLE# 610 | 611 | [Fact:weMustPerformAQuirkafleeg:We must perform a Quirkafleeg?] 612 | The name of the room #R53248(We must perform a Quirkafleeg) is a reference to 613 | issue 5 of the comic book The Adventures of Fat Freddy's Cat, in which 614 | the inhabitants of Pootweet, upon seeing a suitcase full of dead mice, exclaim 615 | 'We must perform a Quirkafleeg!' and promptly lie down on their backs and 616 | start waving their arms and legs in the air. 617 | 618 | [Fact:rescueEsmerelda:Rescue Esmerelda?] 619 | The sequence of rooms #R53504(Up on the Battlements), #R53248(We must perform a 620 | Quirkafleeg), #R52992(I'm sure I've seen this before..) and #R52736(Rescue 621 | Esmerelda) - with the guards, battlements, rope, bells and Esmerelda - are a 622 | reference to 623 | Hunchback 624 | by Ocean Software Ltd. 625 | 626 | [Fact:drJonesWillNeverBelieveThis:Dr Jones will never believe this?] 627 | The name of the room #R59392(Dr Jones will never believe this) may be a 628 | reference to the 'Myth of Jones', a story told by the philosopher Wilfrid 629 | Sellars in his most famous paper, Empiricism and the Philosophy of Mind. 630 | In this story, the hero, Jones, seeks to explain how a person can have a 631 | sensation of something that doesn't exist; for example, how it can be that 632 | people behave just as they would if there were a pink elephant in the room when 633 | in fact there is no such thing there. In this particular room, however, there 634 | is a pink (well, magenta) elephant. 635 | 636 | [Fact:emptiestRooms:The emptiest rooms] 637 | #R62720(Back Door) is the only room that has no items, nasties, rope, arrows or 638 | guardians. 639 | 640 | #ROOM53(back_door) 641 | 642 | #R51712(The Front Door) is the only other room that has no rope, arrows or 643 | guardians; it also has no nasties, but does contain an item. 644 | 645 | #ROOM10(the_front_door) 646 | 647 | [Fact:mostGuardians:The most guardians] 648 | #R54016(The Forgotten Abbey) is the only room with a full complement of eight 649 | guardians. 650 | 651 | #ROOM19(the_forgotten_abbey) 652 | 653 | #R59648(The Attic) is the only other room with eight entities defined: six 654 | vertical guardians and two arrows. 655 | 656 | [Fact:mostItems:The most items] 657 | The room with the most items is #R49152(The Off Licence) with 12. 658 | 659 | #ROOM0(the_off_licence) 660 | -------------------------------------------------------------------------------- /utils/jsw2skool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | import argparse 5 | from collections import OrderedDict 6 | 7 | try: 8 | from skoolkit.snapshot import get_snapshot 9 | from skoolkit import tap2sna, sna2skool 10 | except ImportError: 11 | SKOOLKIT_HOME = os.environ.get('SKOOLKIT_HOME') 12 | if not SKOOLKIT_HOME: 13 | sys.stderr.write('SKOOLKIT_HOME is not set; aborting\n') 14 | sys.exit(1) 15 | if not os.path.isdir(SKOOLKIT_HOME): 16 | sys.stderr.write('SKOOLKIT_HOME={}; directory not found\n'.format(SKOOLKIT_HOME)) 17 | sys.exit(1) 18 | sys.path.insert(0, SKOOLKIT_HOME) 19 | from skoolkit.snapshot import get_snapshot 20 | from skoolkit import tap2sna, sna2skool 21 | 22 | JETSETWILLY_HOME = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 23 | BUILD_DIR = '{}/build'.format(JETSETWILLY_HOME) 24 | JSW_Z80 = '{}/jet_set_willy.z80'.format(BUILD_DIR) 25 | 26 | GUARDIANS = { 27 | 43776: (4, 5), 28 | 43904: (4, 5), 29 | 44032: (4, 66), 30 | 44160: (4, 4), 31 | 44288: (4, 14), 32 | 44416: (4, 7), 33 | 44544: (4, 11), 34 | 44672: (4, 3), 35 | 44800: (4, 5), 36 | 44928: (4, 5), 37 | 45056: (4, (3, 6, 5, 5)), 38 | 45184: (2, 23), 39 | 45248: (2, 6), 40 | 45312: (8, 7), 41 | 45568: (4, 13), 42 | 45696: (4, 74), 43 | 45824: (0, 4), 44 | 46080: (8, 4), 45 | 46336: (8, 67), 46 | 46592: (8, 66), 47 | 46848: (2, 67), 48 | 46912: (2, 3), 49 | 46976: (4, 15), 50 | 47104: (8, 5), 51 | 47360: (4, 13), 52 | 47488: (4, 68), 53 | 47616: (4, 66), 54 | 47744: (4, 52), 55 | 47872: (4, 15), 56 | 48000: (4, 70), 57 | 48128: (8, 66), 58 | 48384: (8, 14), 59 | 48640: (4, 4), 60 | 48768: (4, 69), 61 | 48896: (4, 5), 62 | 49024: (2, 4), 63 | 49088: (2, 22) 64 | } 65 | 66 | class JetSetWilly: 67 | def __init__(self, snapshot): 68 | self.snapshot = snapshot 69 | self.room_names, self.room_names_wp = self._get_room_names() 70 | self.room_macros = self._get_room_macros() 71 | self.room_entities = self._get_room_entities() 72 | 73 | def _get_room_names(self): 74 | rooms = {} 75 | rooms_wp = {} 76 | for a in range(49152, 64768, 256): 77 | room_num = a // 256 - 192 78 | room_name = ''.join([chr(b) for b in self.snapshot[a + 128:a + 160]]).strip() 79 | room_name_wp = room_name 80 | while room_name_wp.find(' ') > 0: 81 | start = room_name_wp.index(' ') 82 | end = start + 2 83 | while room_name_wp[end] == ' ': 84 | end += 1 85 | room_name_wp = '{}#SPACE({}){}'.format(room_name_wp[:start], end - start, room_name_wp[end:]) 86 | rooms[room_num] = room_name 87 | rooms_wp[room_num] = room_name_wp 88 | return rooms, rooms_wp 89 | 90 | def _get_room_macros(self): 91 | room_macros = {} 92 | for room_num in range(61): 93 | addr = 256 * (room_num + 192) 94 | room_macros[room_num] = '#R{}({})'.format(addr, self.room_names_wp[room_num]) 95 | return room_macros 96 | 97 | def _get_room_entities(self): 98 | room_entities = {} 99 | for room_num in range(61): 100 | addr = 256 * (room_num + 192) 101 | for a in range(addr + 240, addr + 256, 2): 102 | num, start = self.snapshot[a:a + 2] 103 | if num == 255: 104 | break 105 | room_entities.setdefault(room_num, []).append((num, start)) 106 | return room_entities 107 | 108 | def _get_room_links(self, room_nums): 109 | macros = [self.room_macros[n] for n in room_nums] 110 | if len(macros) == 1: 111 | return macros[0] 112 | return '{} and {}'.format(', '.join(macros[:-1]), macros[-1]) 113 | 114 | def _get_teleport_code(self, room_num): 115 | code = '' 116 | key = 1 117 | while room_num: 118 | if room_num & 1: 119 | code += str(key) 120 | room_num //= 2 121 | key += 1 122 | return code + '9' 123 | 124 | def _get_guardian_macro(self, addr, attr): 125 | return '#GUARDIAN{},{},{}'.format(addr // 256, (addr % 256) // 32, attr) 126 | 127 | def get_screen_buffer_address_table(self): 128 | lines = ['w 33280 Screen buffer address lookup table '] 129 | lines.append('D 33280 Used by the routines at #R35914, #R37310 and #R38455. ' 130 | 'The value of the Nth entry (0<=N<=127) in this lookup table is the ' 131 | 'screen buffer address for the point with pixel coordinates ' 132 | '(x,y)=(0,N), with the origin (0,0) at the top-left corner.') 133 | lines.append('@ 33280 label=SBUFADDRS') 134 | y = 0 135 | for addr in range(33280, 33536, 2): 136 | lines.append('W {} y={}'.format(addr, y)) 137 | y += 1 138 | lines.append('i 33536') 139 | return '\n'.join(lines) 140 | 141 | def get_entity_definitions(self): 142 | defs = {} 143 | sprite_addrs = {} 144 | for room_num, specs in self.room_entities.items(): 145 | room_bg = self.snapshot[(room_num + 192) * 256 + 160] & 56 146 | for num, start in specs: 147 | defs.setdefault(num, set()).add(room_num) 148 | def_addr = 40960 + num * 8 149 | guardian_def = self.snapshot[def_addr:def_addr + 8] 150 | guardian_type = guardian_def[0] & 7 151 | if guardian_type & 3 in (1, 2): 152 | frame_addrs = [] 153 | base_addr = 256 * guardian_def[5] 154 | base_index = start & 224 155 | for i in range(0, 256, 32): 156 | frame_addr = base_addr + (guardian_def[1] & i | base_index) 157 | if frame_addr not in frame_addrs: 158 | frame_addrs.append(frame_addr) 159 | sprite_addrs.setdefault(num, set()).add((tuple(frame_addrs), room_bg)) 160 | 161 | lines = ['b 40960 Entity definitions'] 162 | lines.append('@ 40960 label=ENTITYDEFS') 163 | lines.append('D 40960 Used by the routine at #R35090.') 164 | lines.append('N 40960 The following (empty) entity definition (#b0) is copied into one of the entity buffers at #R33024 for any entity specification whose first byte is zero.') 165 | lines.append('B 40960,8') 166 | for num in range(1, 112): 167 | addr = 40960 + num * 8 168 | if num in defs: 169 | room_links = self._get_room_links(sorted(defs[num])) 170 | comment = 'The following entity definition (#b{}) is used in {}.'.format(num, room_links) 171 | else: 172 | comment = 'The following entity definition (#b{}) is not used.'.format(num) 173 | lines.append('N {} {}'.format(addr, comment)) 174 | base_indexes = set() 175 | if num in sprite_addrs: 176 | ink = self.snapshot[40961 + 8 * num] & 7 177 | bright = 8 * (self.snapshot[40961 + 8 * num] & 8) 178 | rows = [] 179 | for frame_addrs, room_bg in sorted(sprite_addrs[num]): 180 | macros = [] 181 | attr = bright | room_bg | ink 182 | for a in frame_addrs: 183 | macros.append(self._get_guardian_macro(a, attr)) 184 | base_indexes.add((a & 255) // 32) 185 | rows.append('{ ' + ' | '.join(macros) + ' }') 186 | lines.append('N {} #UDGTABLE {} TABLE#'.format(addr, ' '.join(rows))) 187 | if num in defs or num == 43: 188 | entity_def = self.snapshot[addr:addr + 8] 189 | entity_type = entity_def[0] & 7 190 | direction = 'left to right' if entity_def[0] & 128 else 'right to left' 191 | ink = entity_def[1] & 7 192 | bright = (entity_def[1] & 8) // 8 193 | frame_mask = '{:08b}'.format(entity_def[1])[:3] 194 | b1_format = ',b1' 195 | desc1 = 'INK {} (bits 0-2), BRIGHT {} (bit 3), animation frame mask {} (bits 5-7)'.format(ink, bright, frame_mask) 196 | b6_format = '' 197 | if entity_type & 3 == 1: 198 | desc0 = 'Horizontal guardian (bits 0-2), initial animation frame {} (bits 5 and 6), initially moving {} (bit 7)'.format((entity_def[0] & 96) // 32, direction) 199 | desc2 = 'Replaced by the base sprite index and initial x-coordinate (copied from the second byte of the entity specification in the room definition)' 200 | desc3 = 'Pixel y-coordinate: {}'.format(entity_def[3] // 2) 201 | desc4 = 'Unused' 202 | desc5 = 'Page containing the sprite graphic data: #R{}(#b{})'.format(entity_def[5] * 256, entity_def[5]) 203 | desc6 = 'Minimum x-coordinate: {}'.format(entity_def[6]) 204 | desc7 = 'Maximum x-coordinate: {}'.format(entity_def[7]) 205 | elif entity_type & 3 == 2: 206 | desc0_infix = '' 207 | if frame_mask != '000': 208 | desc0_infix = ', animation frame updated on every {}pass (bit 4)'.format('' if entity_def[0] & 16 else 'second ') 209 | desc0 = 'Vertical guardian (bits 0-2){}, initial animation frame {} (bits 5 and 6)'.format(desc0_infix, (entity_def[0] & 96) // 32) 210 | desc2 = 'Replaced by the base sprite index and x-coordinate (copied from the second byte of the entity specification in the room definition)' 211 | desc3 = 'Initial pixel y-coordinate: {}'.format(entity_def[3] // 2) 212 | y_inc = entity_def[4] if entity_def[4] < 128 else entity_def[4] - 256 213 | if y_inc == 0: 214 | direction = 'not moving' 215 | elif y_inc > 0: 216 | direction = 'moving down' 217 | else: 218 | direction = 'moving up' 219 | desc4 = 'Initial pixel y-coordinate increment: {} ({})'.format(y_inc // 2, direction) 220 | page = entity_def[5] 221 | if page == 156: 222 | links = [] 223 | if 2 in base_indexes: 224 | e_addr = 40000 225 | links.append('#R40000(foot)') 226 | if 3 in base_indexes: 227 | e_addr = 40000 228 | links.append('#R40000(barrel)') 229 | if base_indexes & {4, 6}: 230 | e_addr = 40064 231 | links.append('#R40064(Maria)') 232 | if len(links) == 1: 233 | desc5 = 'Page containing the sprite graphic data: #R{}(#b{})'.format(e_addr, page) 234 | else: 235 | desc5 = 'Page containing the sprite graphic data: #b{}#HTML[ ({})]'.format(page, ', '.join(links)) 236 | else: 237 | anchor = '#43776' if page == 171 else '' 238 | desc5 = 'Page containing the sprite graphic data: #R{}{}(#b{})'.format(page * 256, anchor, page) 239 | desc6 = 'Minimum pixel y-coordinate: {}'.format(entity_def[6] // 2) 240 | desc7 = 'Maximum pixel y-coordinate: {}'.format(entity_def[7] // 2) 241 | elif entity_type & 3 == 3: 242 | desc0 = 'Rope (bits 0-2), initially swinging {} (bit 7)'.format(direction) 243 | b1_format = '' 244 | desc1 = 'Initial animation frame index' 245 | desc2 = 'Replaced by the x-coordinate of the top of the rope (copied from the second byte of the entity specification in the room definition)' 246 | desc3 = 'Unused' 247 | desc4 = 'Length' 248 | desc5 = 'Unused' 249 | desc6 = 'Unused' 250 | desc7 = 'Animation frame at which the rope changes direction' 251 | else: 252 | desc0 = 'Arrow (bits 0-2), flying {} (bit 7)'.format(direction) 253 | b1_format = '' 254 | desc1 = 'Unused' 255 | desc2 = 'Replaced by the y-coordinate (copied from the second byte of the entity specification in the room definition)' 256 | desc3 = 'Unused' 257 | desc4 = 'Initial x-coordinate: {}'.format(entity_def[4]) 258 | desc5 = 'Unused' 259 | b6_format = ',b1' 260 | desc6 = 'Top/bottom pixel row (drawn either side of the shaft)' 261 | desc7 = 'Unused' 262 | lines.append('@ {} label=ENTITY{}'.format(addr, num)) 263 | lines.append('B {},b1 {}'.format(addr, desc0)) 264 | lines.append('B {}{} {}'.format(addr + 1, b1_format, desc1)) 265 | lines.append('B {} {}'.format(addr + 2, desc2)) 266 | lines.append('B {} {}'.format(addr + 3, desc3)) 267 | lines.append('B {} {}'.format(addr + 4, desc4)) 268 | lines.append('B {} {}'.format(addr + 5, desc5)) 269 | lines.append('B {}{} {}'.format(addr + 6, b6_format, desc6)) 270 | lines.append('B {} {}'.format(addr + 7, desc7)) 271 | else: 272 | lines.append('B {},8'.format(addr)) 273 | lines.append('N 41856 The next 15 entity definitions (#b112-#b126) are unused.') 274 | lines.append('B 41856,120,8') 275 | lines.append('@ 41976 label=ENTITY127') 276 | lines.append('N 41976 The following entity definition (#b127) - whose eighth byte is at #R41983 - ' 277 | 'is copied into one of the entity buffers at #R33024 for any entity specification whose ' 278 | 'first byte is #b127 or #b255; the first byte of the definition (#b255) serves to ' 279 | 'terminate the entity buffers.') 280 | lines.append('B 41976,7') 281 | lines.append('i 41983') 282 | return '\n'.join(lines) 283 | 284 | def get_guardian_graphics(self): 285 | guardians = {} 286 | for room_num, specs in self.room_entities.items(): 287 | for num, start in specs: 288 | def_addr = 40960 + num * 8 289 | guardian_def = self.snapshot[def_addr:def_addr + 8] 290 | guardian_type = guardian_def[0] & 7 291 | if guardian_type & 3 in (1, 2): 292 | sprite_addr = 256 * guardian_def[5] + (start & 224) 293 | if sprite_addr >= 43776: 294 | while sprite_addr not in GUARDIANS: 295 | sprite_addr -= 32 296 | guardians.setdefault(sprite_addr, set()).add(room_num) 297 | 298 | lines = ['b 43776 Guardian graphics'] 299 | lines.append('@ 43776 label=GUARDIANS') 300 | lines.append('D 43776 Used by the routine at #R37310.') 301 | lines.append('@ 46592 label=FLYINGPIG0') 302 | for a in sorted(GUARDIANS.keys()): 303 | page = a // 256 304 | base_index = (a % 256) // 32 305 | num, attr = GUARDIANS[a] 306 | if isinstance(attr, int): 307 | attrs = [attr] * num 308 | else: 309 | attrs = list(attr) 310 | end_index = base_index + num - 1 311 | if a in guardians: 312 | room_links = self._get_room_links(sorted(guardians[a])) 313 | comment = 'This guardian (page #b{}, sprites {}-{}) appears in {}.'.format(page, base_index, end_index, room_links) 314 | elif a == 45312: 315 | comment = 'This guardian (page #b{}, sprites {}-{}) is not used.'.format(page, base_index, end_index) 316 | elif a == 45824: 317 | comment = 'The next 256 bytes are unused.' 318 | lines.append('N {} {}'.format(a, comment)) 319 | if num: 320 | sprites = [] 321 | for addr in range(a, a + num * 32, 32): 322 | sprites.append(self._get_guardian_macro(addr, attrs.pop(0))) 323 | lines.append('N {} #UDGTABLE {{ {} }} TABLE#'.format(a, ' | '.join(sprites))) 324 | lines.append('B {},{},16'.format(a, 32 * num)) 325 | else: 326 | lines.append('S {},256'.format(a)) 327 | lines.append('i 49152') 328 | return '\n'.join(lines) 329 | 330 | def get_item_table(self): 331 | lines = ['b 41984 Item table'] 332 | lines.append('D 41984 Used by the routines at #R34762 and #R37841.') 333 | lines.append('D 41984 The location of item N (#b173<=N<=#b255) is defined by the pair of bytes at ' 334 | 'addresses #R41984+N and #R42240+N. The meaning of the bits in each byte-pair is ' 335 | 'as follows:') 336 | lines.append('D 41984 #TABLE(default,centre) ' 337 | '{ =h Bit(s) | =h Meaning } ' 338 | '{ 15 | Most significant bit of the y-coordinate } ' 339 | '{ 14 | Collection flag (reset=collected, set=uncollected) } ' 340 | '{ 8-13 | Room number } ' 341 | '{ 5-7 | Least significant bits of the y-coordinate } ' 342 | '{ 0-4 | x-coordinate } TABLE#') 343 | lines.append('@ 41984 label=ITEMTABLE1') 344 | lines.append('S 41984 Unused') 345 | items = {} 346 | for a in range(42157, 42240): 347 | b1, b2 = self.snapshot[a], self.snapshot[a + 256] 348 | x, y = b2 & 31, 8 * (b1 >> 7) + b2 // 32 349 | room_link = self._get_room_links([b1 & 63]) 350 | index = a % 256 351 | items[index] = (room_link, (x, y)) 352 | if a == 42183: 353 | lines.append('@ {} bfix=DEFB 11'.format(a)) 354 | lines.append('B {} Item #b{} at ({},{}) in {}'.format(a, index, y, x, room_link)) 355 | lines.append('@ 42240 label=ITEMTABLE2') 356 | lines.append('S 42240 Unused') 357 | for a in range(42413, 42496): 358 | index = a % 256 359 | room_link, (x, y) = items[index] 360 | lines.append('B {} Item #b{} at ({},{}) in {}'.format(a, index, y, x, room_link)) 361 | lines.append('i 42496') 362 | return '\n'.join(lines) 363 | 364 | def _write_tiles(self, lines, a): 365 | room_num = a // 256 - 192 366 | tiles_table = '#UDGTABLE {{ #TILES{} }} TABLE#'.format(room_num) 367 | comment = 'The next 54 bytes are copied to #R32928 and contain the attributes and graphic data for the tiles used to build the room.' 368 | if room_num == 36: 369 | comment += ' Note that because of a #BUG(corruptedNasties)(bug) in the game engine, the nasty tile is not drawn correctly (see the room image above).' 370 | tile_usage = [' (unused)'] * 6 371 | for b in self.snapshot[a:a + 128]: 372 | for i in range(4): 373 | tile_usage[b & 3] = '' 374 | b >>= 2 375 | ramp_length = self.snapshot[a + 221] 376 | if ramp_length: 377 | tile_usage[4] = '' 378 | conveyor_length = self.snapshot[a + 217] 379 | if conveyor_length: 380 | tile_usage[5] = '' 381 | conveyor_attr = self.snapshot[a + 205] 382 | b = a + 160 383 | while b < a + 205 and self.snapshot[b] != conveyor_attr: 384 | b += 1 385 | if b < a + 205: 386 | comment += ' Note that because of a #BUG(corruptedConveyors)(bug) in the game engine, the conveyor tile is not drawn correctly (see the room image above).' 387 | lines.append('N {} {}'.format(a + 160, comment)) 388 | lines.append('N {} {}'.format(a + 160, tiles_table)) 389 | if a == 57088: 390 | lines.append('@ 57248 bfix=DEFB 5,0,0,0,0,0,0,0,0') 391 | lines.append('B {},9,9 Background{}'.format(a + 160, tile_usage[0])) 392 | lines.append('B {},9,9 Floor{}'.format(a + 169, tile_usage[1])) 393 | lines.append('B {},9,9 Wall{}'.format(a + 178, tile_usage[2])) 394 | lines.append('B {},9,9 Nasty{}'.format(a + 187, tile_usage[3])) 395 | lines.append('B {},9,9 Ramp{}'.format(a + 196, tile_usage[4])) 396 | if a == 56576: 397 | lines.append('@ 56781 bfix=DEFB 2,165,255,90,255,255,170,85,170') 398 | lines.append('B {},9,9 Conveyor{}'.format(a + 205, tile_usage[5])) 399 | 400 | def _get_coordinates(self, lsb, msb): 401 | if 94 <= msb <= 95: 402 | x = lsb & 31 403 | y = 8 * (msb & 1) + (lsb & 224) // 32 404 | return y, x 405 | 406 | def _write_conveyor(self, lines, a): 407 | lines.append('N {} The next four bytes are copied to #R32982 and specify the direction, location and length of the conveyor.'.format(a + 214)) 408 | if a == 58112: 409 | lines.append('@ {} bfix=DEFB 254'.format(a + 214)) 410 | conveyor_d, p1, p2 = self.snapshot[a + 214:a + 217] 411 | coords = self._get_coordinates(p1, p2) 412 | location_suffix = ': ({},{})'.format(*coords) if coords else ' (unused)' 413 | length_suffix = '' if self.snapshot[a + 217] else ': 0 (there is no conveyor in this room)' 414 | lines.append('B {},1 Direction ({})'.format(a + 214, 'right' if conveyor_d else 'left')) 415 | lines.append('W {},2 Location in the attribute buffer at #R24064{}'.format(a + 215, location_suffix)) 416 | lines.append('B {},1 Length{}'.format(a + 217, length_suffix)) 417 | 418 | def _write_ramp(self, lines, a): 419 | lines.append('N {} The next four bytes are copied to #R32986 and specify the direction, location and length of the ramp.'.format(a + 218)) 420 | ramp_d, p1, p2 = self.snapshot[a + 218:a + 221] 421 | coords = self._get_coordinates(p1, p2) 422 | location_suffix = ': ({},{})'.format(*coords) if coords else ' (unused)' 423 | length_suffix = '' if self.snapshot[a + 221] else ': 0 (there is no ramp in this room)' 424 | lines.append('B {},1 Direction (up to the {})'.format(a + 218, 'right' if ramp_d else 'left')) 425 | lines.append('W {},2 Location in the attribute buffer at #R24064{}'.format(a + 219, location_suffix)) 426 | lines.append('B {},1 Length{}'.format(a + 221, length_suffix)) 427 | 428 | def _write_exits(self, lines, a, room_name): 429 | lines.append('N {} The next four bytes are copied to #R33001 and specify the rooms to the left, to the right, above and below.'.format(a + 233)) 430 | room_left, room_right, room_up, room_down = self.snapshot[a + 233:a + 237] 431 | for addr, num, name, desc in ( 432 | (a + 233, room_left, self.room_names_wp.get(room_left), 'to the left'), 433 | (a + 234, room_right, self.room_names_wp.get(room_right), 'to the right'), 434 | (a + 235, room_up, self.room_names_wp.get(room_up), 'above'), 435 | (a + 236, room_down, self.room_names_wp.get(room_down), 'below'), 436 | ): 437 | if name and name != room_name: 438 | lines.append('B {} Room {} (#R{}({}))'.format(addr, desc, 256 * (num + 192), name)) 439 | elif name: 440 | lines.append('B {} Room {} ({})'.format(addr, desc, name)) 441 | else: 442 | lines.append('B {} Room {} (none)'.format(addr, desc)) 443 | 444 | def _write_entity_specs(self, lines, a): 445 | room_num = a // 256 - 192 446 | start = a + 240 447 | entities = [] 448 | for addr in range(start, a + 256, 2): 449 | num, coords = self.snapshot[addr:addr + 2] 450 | def_addr = 40960 + (num & 127) * 8 451 | entity_def = self.snapshot[def_addr:def_addr + 8] 452 | guardian_type = entity_def[0] & 7 453 | entities.append((num, coords, guardian_type, def_addr)) 454 | infix = '' if room_num == 47 else 'are copied to #R33008 and ' 455 | lines.append('N {} The next eight pairs of bytes {}specify the entities (ropes, arrows, guardians) in this room.'.format(start, infix)) 456 | addr = start 457 | terminated = False 458 | for num, coords, guardian_type, def_addr in entities: 459 | anchor = '' 460 | if num == 0: 461 | desc = 'Nothing' 462 | anchor = '#{}'.format(def_addr) 463 | elif num == 255: 464 | desc = 'Terminator' 465 | terminated = True 466 | elif guardian_type == 1: 467 | desc = 'Guardian no. #b{} (horizontal), base sprite {}, initial x={}'.format(num, coords // 32, coords & 31) 468 | elif guardian_type == 2: 469 | desc = 'Guardian no. #b{} (vertical), base sprite {}, x={}'.format(num, coords // 32, coords & 31) 470 | elif guardian_type == 3: 471 | desc = 'Rope at x={}'.format(coords & 31) 472 | else: 473 | direction = ('right to left', 'left to right')[self.snapshot[def_addr] // 128] 474 | if coords & 1: 475 | # Faulty arrow specification 476 | y0 = self.snapshot[coords + 33281] - 96 477 | pixel_y = 8 * (y0 & 248) + (y0 & 7) + (self.snapshot[coords + 33280] & 224) // 4 478 | else: 479 | pixel_y = coords // 2 480 | desc = 'Arrow flying {} at pixel y-coordinate {}'.format(direction, pixel_y) 481 | suffix = ' (unused)' if 0 < num < 255 and terminated else '' 482 | if addr == 59900: 483 | lines.append('@ {} bfix=DEFB 69,82'.format(addr)) 484 | lines.append('B {},2 {} (#R{}{}){}'.format(addr, desc, def_addr, anchor, suffix)) 485 | addr += 2 486 | 487 | def get_rooms(self): 488 | lines = [] 489 | 490 | start = 41984 + self.snapshot[41983] 491 | items = {} 492 | for a in range(start, 42240): 493 | b1 = self.snapshot[a] 494 | b2 = self.snapshot[a + 256] 495 | room_num = b1 & 63 496 | x = b2 & 31 497 | y = 8 * (b1 >> 7) + b2 // 32 498 | items.setdefault(room_num, []).append((x, y)) 499 | 500 | for a in range(49152, 64768, 256): 501 | room = self.snapshot[a:a + 256] 502 | room_num = a // 256 - 192 503 | room_name = self.room_names[room_num] 504 | if room_num == 60: 505 | lines.append('@ {} ignoreua:t'.format(a)) 506 | lines.append('b {} Room #b{}: {} (teleport: {})'.format(a, room_num, self.room_names_wp[room_num], self._get_teleport_code(room_num))) 507 | if a == 61184: 508 | # [ 509 | room_image = '#ROOM{}(left_square_bracket)'.format(room_num) 510 | lines.append('D 61184 This room is not used.') 511 | else: 512 | room_image = '#ROOM{}({})'.format(room_num, room_name.lower().replace(' ', '_')) 513 | lines.append('D {} Used by the routine at #R35090.'.format(a)) 514 | lines.append('D {} #UDGTABLE {{ {} }} TABLE#'.format(a, room_image)) 515 | if room_num == 47: 516 | lines.append('D 61184 The first 128 bytes define the room layout. Each bit-pair (bits 7 and 6, 5 and 4, 3 and 2, or 1 and 0 of each byte) determines the type of tile (background, floor, wall or nasty) that will be drawn at the corresponding location.') 517 | lines.append('B 61184,128,8 Room layout (completely empty)') 518 | else: 519 | lines.append('D {} The first 128 bytes are copied to #R32768 and define the room layout. Each bit-pair (bits 7 and 6, 5 and 4, 3 and 2, or 1 and 0 of each byte) determines the type of tile (background, floor, wall or nasty) that will be drawn at the corresponding location.'.format(a)) 520 | if room_num == 30: 521 | lines.append('@ 56872 bfix=DEFB 0,0,0,129,4,0,0,0') 522 | elif room_num == 43: 523 | lines.append('@ 60224 bfix=DEFB 0,0,0,0,0,48,195,0') 524 | lines.append('B {},128,8 Room layout'.format(a)) 525 | 526 | # Room name 527 | if room_num == 47: 528 | lines.append('N 61312 The next 32 bytes specify the room name.') 529 | else: 530 | lines.append('N {} The next 32 bytes are copied to #R32896 and specify the room name.'.format(a + 128)) 531 | lines.append('T {},32 Room name'.format(a + 128)) 532 | 533 | if room_num == 47: 534 | lines.append('N 61344 In a working room definition, the next 80 bytes define the tiles, conveyor, ramp, border colour, item graphic, and exits. In this room, however, there are code remnants and unused data.') 535 | lines.append('B 61344,9,9 Background tile') 536 | lines.append('B 61353') 537 | lines.append('C 61361') 538 | lines.append('B 61370') 539 | lines.append('C 61372') 540 | lines.append('B 61399') 541 | lines.append('B 61401 Conveyor length (deliberately set to 0)') 542 | lines.append('C 61402') 543 | lines.append('B 61405 Ramp length (deliberately set to 0)') 544 | lines.append('C 61406') 545 | lines.append('B 61423') 546 | else: 547 | # Tiles 548 | self._write_tiles(lines, a) 549 | 550 | # Conveyor direction, location and length 551 | self._write_conveyor(lines, a) 552 | 553 | # Ramp direction, location and length 554 | self._write_ramp(lines, a) 555 | 556 | # Border colour 557 | lines.append('N {} The next byte is copied to #R32990 and specifies the border colour.'.format(a + 222)) 558 | lines.append('B {} Border colour'.format(a + 222)) 559 | 560 | # Bytes 223/224 561 | lines.append('N {} The next two bytes are copied to #R32991, but are not used.'.format(a + 223)) 562 | lines.append('B {} Unused'.format(a + 223)) 563 | 564 | # Item graphic 565 | lines.append('N {} The next eight bytes are copied to #R32993 and define the item graphic.'.format(a + 225)) 566 | lines.append('N {} #UDGTABLE {{ #ITEM{} }} TABLE#'.format(a + 225, room_num)) 567 | lines.append('B {},8,8 Item graphic{}'.format(a + 225, '' if items.get(room_num) else ' (unused)')) 568 | 569 | # Rooms to the left, to the right, above and below 570 | self._write_exits(lines, a, room_name) 571 | 572 | # Bytes 237-239 573 | lines.append('N {} The next three bytes are copied to #R33005, but are not used.'.format(a + 237)) 574 | lines.append('B {} Unused'.format(a + 237)) 575 | 576 | # Entities 577 | self._write_entity_specs(lines, a) 578 | 579 | lines.append('i 64768') 580 | return '\n'.join(lines) 581 | 582 | def run(subcommand): 583 | if not os.path.isdir(BUILD_DIR): 584 | os.mkdir(BUILD_DIR) 585 | if not os.path.isfile(JSW_Z80): 586 | tap2sna.main(('-d', BUILD_DIR, '@{}/jet_set_willy.t2s'.format(JETSETWILLY_HOME))) 587 | jsw = JetSetWilly(get_snapshot(JSW_Z80)) 588 | ctlfile = '{}/{}.ctl'.format(BUILD_DIR, subcommand) 589 | with open(ctlfile, 'wt') as f: 590 | f.write(getattr(jsw, methods[subcommand][0])()) 591 | sna2skool.main(('-c', ctlfile, JSW_Z80)) 592 | 593 | ############################################################################### 594 | # Begin 595 | ############################################################################### 596 | methods = OrderedDict(( 597 | ('entity-defs', ('get_entity_definitions', 'Entity definitions (40960-41982)')), 598 | ('guardians', ('get_guardian_graphics', 'Guardian graphics (43776-49151)')), 599 | ('items', ('get_item_table', 'Item table (41984-42495)')), 600 | ('rooms', ('get_rooms', 'Rooms (49152-64767)')), 601 | ('sbat', ('get_screen_buffer_address_table', 'Screen buffer address table (33280-33535)')) 602 | )) 603 | subcommands = '\n'.join(' {} - {}'.format(k, v[1]) for k, v in methods.items()) 604 | parser = argparse.ArgumentParser( 605 | usage='%(prog)s SUBCOMMAND', 606 | description="Produce a skool file snippet for Jet Set Willy. SUBCOMMAND must be one of:\n\n{}".format(subcommands), 607 | formatter_class=argparse.RawTextHelpFormatter, 608 | add_help=False 609 | ) 610 | parser.add_argument('subcommand', help=argparse.SUPPRESS, nargs='?') 611 | namespace, unknown_args = parser.parse_known_args() 612 | if unknown_args or namespace.subcommand not in methods: 613 | parser.exit(2, parser.format_help()) 614 | run(namespace.subcommand) 615 | -------------------------------------------------------------------------------- /sources/jsw.ref: -------------------------------------------------------------------------------- 1 | ; Copyright 2012-2022, 2025 Richard Dymond (rjdymond@gmail.com) 2 | 3 | [Config] 4 | Expand=#INCLUDE(Expand) 5 | HtmlWriterClass=:jetsetwilly.JetSetWillyHtmlWriter 6 | RefFiles=bugs.ref;changelog.ref;facts.ref;glossary.ref;pokes.ref;sound.ref 7 | GameDir=jet_set_willy 8 | 9 | [Game] 10 | Game=Jet Set Willy 11 | Logo=#LOGO0,0(logo) 12 | StyleSheet=skoolkit.css;jsw.css 13 | Copyright=© 1984 Software Projects Ltd. © 2022 Richard Dymond. 14 | Release=The complete Jet Set Willy RAM disassembly 20221122 15 | LinkInternalOperands=1 16 | AddressAnchor={address#IF({base}==16)(:04x)} 17 | 18 | [PageHeaders] 19 | Asm-t=Messages 20 | 21 | [Index:Graphics:Graphics and sound] 22 | Rooms 23 | Guardians 24 | Sound 25 | 26 | [Index:DataTables:Data tables and buffers] 27 | GameStatusBuffer 28 | Items 29 | Entity definitions 30 | Codes 31 | 32 | [Index:Reference:Reference] 33 | Changelog 34 | Glossary 35 | Facts 36 | Bugs 37 | Pokes 38 | Credits 39 | 40 | [Paths] 41 | Credits=reference/credits.html 42 | Rooms=tables/rooms.html 43 | Codes=tables/codes.html 44 | CodeFiles={address#IF({base}==16)(:04#S/X/)}.html 45 | Sound=sound/sound.html 46 | 47 | [MemoryMap:MemoryMap] 48 | PageByteColumns=#IF({base}==16)(0,1) 49 | 50 | [MemoryMap:DataMap] 51 | PageByteColumns=#IF({base}==16)(0,1) 52 | 53 | [MemoryMap:UnusedMap] 54 | Intro=#INCLUDE1(UnusedMapIntro) 55 | PageByteColumns=#IF({base}==16)(0,1) 56 | 57 | [UnusedMapIntro] 58 | In addition to the entries in the table below, the following embedded sections 59 | of code and data are unused: 60 | 61 | #LIST 62 | { The entries at #R33622(#N33622-#N33663) and #R33750(#N33750-#N33791) in the 63 | rope animation table (84 bytes) } 64 | { The section of code at #R35345(#N35345-#N35365) in the main loop (21 bytes) } 65 | { Entity definition #b43 at #R41304 (8 bytes) } 66 | { Entity definition #b63 at #R41464 (8 bytes) } 67 | { Entity definitions #b112-#b126 at #R41856 (120 bytes) } 68 | { Slots #b0-#b172 at #R41984#41984 and #R42240 in the item table (346 bytes) } 69 | { Pages #R45312(#b177) and #R45824(#b179) in the guardian graphic data (512 70 | bytes) } 71 | { Room #R61184(#b47) (256 bytes) } 72 | LIST# 73 | 74 | [Page:Rooms] 75 | PageContent=#INCLUDE(Rooms) 76 | 77 | [Rooms] 78 | #TABLE(default,centre,centre,,centre) 79 | { =h No. | =h Address | =h Name | =h Teleport } 80 | #FOR(0,60)(rnum, 81 | #LET(addr=49152+rnum*256) 82 | #LET(name$=#STR({addr}+128,7,32)) 83 | #LET(tcode=#FOR(1,6)(key,#IF(rnum&(1<<(key-1)))(key))9) 84 | { #N(rnum,,,1)(0x) | #N({addr}) | #FORMAT(#R({addr})({name$}) | {tcode}) } 85 | ) 86 | TABLE# 87 | 88 | [Page:Guardians] 89 | Content=asm/#N43776.html 90 | 91 | [Page:Items] 92 | Content=asm/#N41984.html 93 | 94 | [Page:Entity definitions] 95 | Content=asm/#N40960.html 96 | 97 | [Page:Codes] 98 | PageContent=#INCLUDE(Codes) 99 | 100 | [Codes] 101 | #TABLE(default,centre,centre,centre,centre) 102 | { =h Address | =h Value | =h Grid | =h Code } 103 | #FOR(40448,40626)(addr, 104 | #LET(gridloc$=#CHR(65+(addr&255)%18,1)#CHR(48+(addr&255)/18,1)) 105 | #LET(v=#PEEKaddr+addr%256) 106 | #LET(code=#FOR(6,0,-2)(q,#EVAL((({v}>>q)&3)+1))) 107 | { #Naddr | #N(#PEEKaddr,,,1)(0x) | #FORMAT({gridloc$} | {code}) } 108 | ) 109 | TABLE# 110 | 111 | [Page:Credits] 112 | PageContent=#INCLUDE(Credits) 113 | 114 | [Credits] 115 |
116 | This disassembly would have taken a lot longer to finish if it weren't for the 117 | research and documentation already done and made available by: 118 | #LIST 119 | { Andrew 120 | Broad (and Carl Woffenden) } 121 | { J. G. Harston } 122 | LIST# 123 | In particular, their partial disassemblies, bug descriptions and room format 124 | descriptions were of great help. 125 |
126 | 127 | [Expand] 128 | #DEF(#REM()(remark)) 129 | 130 | #DEF(#LOGO(dark,trans)(fname) 131 | #JSW($dark+$trans){4,4,200,120}({ImagePath}/$fname|Jet Set Willy) 132 | ) 133 | 134 | #DEF(#GUARDIAN(addr,index,attr) 135 | #LET(a=$addr*256+$index*32) 136 | #UDGARRAY(2,$attr,,2)(({a})-({a}+17)-1-16)(guardian$addr-$index-$attr*) 137 | ) 138 | 139 | #DEF(#TILES(rnum) 140 | #LET(a=49312+256*$rnum) 141 | #UDG({a}+1,#PEEK({a}))(background#EVAL$rnum,,2) | 142 | #UDG({a}+10,#PEEK({a}+9))(floor#EVAL$rnum,,2) | 143 | #UDG({a}+19,#PEEK({a}+18))(wall#EVAL$rnum,,2) | 144 | #UDG({a}+28,#PEEK({a}+27))(nasty#EVAL$rnum,,2) | 145 | #UDG({a}+37,#PEEK({a}+36))(ramp#EVAL$rnum,,2) | 146 | #UDG({a}+46,#PEEK({a}+45))(conveyor#EVAL$rnum,,2) 147 | ) 148 | 149 | #DEF(#ITEM(rnum) 150 | #UDG(49377+256*$rnum,(#PEEK(49312+256*$rnum)&120)+3)(item#EVAL$rnum,,2) 151 | ) 152 | 153 | #DEF(#SPRITE(addr,attr) 154 | #UDGARRAY(2,$attr,2,2)($addr-($addr+17)-1-16) 155 | ) 156 | 157 | #DEF(#TILE(addr)(frame) 158 | #UDG($addr+1,#PEEK$addr)(*$frame) 159 | ) 160 | 161 | #DEF(#SFRAME(addr,attr)(frame) 162 | #UDGARRAY(2,$attr,,2)($addr-($addr+17)-1-16)(*$frame) 163 | ) 164 | 165 | #DEF(#BTILEADDR(addr,attr) 166 | #LET(ta=$addr+160) 167 | #WHILE(#PEEK({ta})!=$attr)(#LET(ta={ta}+1)) 168 | #EVAL({ta}) 169 | ) 170 | 171 | #DEF(#ITEMS(addr,x,y)(frame) 172 | #LET(rnum=($addr-49152)/256) 173 | #LET(ink=3) 174 | #UDG($addr+225)(*item) 175 | #FOR(41984+#PEEK41983,42239)(q, 176 | #LET(b1=#PEEKq) 177 | #IF({b1}&63=={rnum})( 178 | #LET(b2=#PEEK(q+256)) 179 | #LET(x=({b2}&31)-$x) 180 | #LET(y=8*({b1}>>7)+{b2}/32-$y) 181 | #OVER({x},{y},,,3)({ink}+($b&248))($f)($frame,item) 182 | #LET(ink={ink}+1) 183 | #IF({ink}==7)(#LET(ink=3)) 184 | ) 185 | ) 186 | ) 187 | 188 | #DEF(#ARROW(addr,start,x,y)(frame) 189 | #LET(dfaddr=#PEEK($start+33280)+256*(#PEEK($start+33281)-32)) 190 | #IF(16384<={dfaddr}<20480)( 191 | #LET(py=64*(({dfaddr}-16384)/2048)+({dfaddr}%256)/4-$y*8) 192 | #LET(ydelta=({dfaddr}/256)&7) 193 | #LET(x=#PEEK($addr+4)&31-$x) 194 | #POKES(8,0,8) 195 | #POKES(8+{ydelta}-1,#PEEK($addr+6),2,2) 196 | #POKES(8+{ydelta},255) 197 | #UDG(8,7)(*arrow) 198 | #OVER({x},0,0,{py},1)($b&56|$f&71)($frame,arrow) 199 | ) 200 | ) 201 | 202 | #DEF(#ROPE(addr,start,x,y)(frame) 203 | #LET(length=#PEEK($addr+4)) 204 | #UDGARRAY1(15616x(({length}*3)/8+1))(*rope) 205 | #FOR(0,{length})(y,#PLOT(0,y*3)(rope)) 206 | #OVER(($start&31)-$x,-$y,0,0)($frame,rope) 207 | ) 208 | 209 | #DEF(#OGUARDIAN(addr,start,x,y)(frame) 210 | #LET(b=#PEEK($addr+1)) 211 | #LET(saddr=($start&224)+256*#PEEK($addr+5)) 212 | #SFRAME({saddr},8*({b}&8)+({b}&7))(guardian) 213 | #LET(x=($start&31)-$x) 214 | #LET(py=#PEEK($addr+3)/2-$y*8) 215 | #OVER({x},0,0,{py},1)($b&56|$f&71)($frame,guardian) 216 | ) 217 | 218 | #DEF(#GUARDIANS(addr,x,y)(frame) 219 | #LET(done=0) 220 | #FOR($addr+240,$addr+254,2)(z, 221 | #LET(num=#PEEKz) 222 | #IF({num}==255)(#LET(done=1)) 223 | #IF({done}==0)( 224 | #LET(start=#PEEK(z+1)) 225 | #LET(defaddr=40960+{num}*8) 226 | #LET(gtype=#PEEK({defaddr})&7) 227 | #IF(0<{gtype}&3<3)(#OGUARDIAN({defaddr},{start},$x,$y)($frame)) 228 | #IF({gtype}&3==3)(#ROPE({defaddr},{start},$x,$y)($frame)) 229 | #IF({gtype}==4)(#ARROW({defaddr},{start},$x,$y)($frame)) 230 | ) 231 | ) 232 | #IF($addr==57600)( 233 | #SFRAME(42496,7)(toilet) 234 | #OVER(28,13,,,1)($b&56|$f&71)($frame,toilet) 235 | ) 236 | #IF($addr==58112)( 237 | #UDGARRAY(2,7,,2)(40064,69;40065,69;40080;40081)(*maria) 238 | #OVER(14,11,,,1)($b&56|$f&71)($frame,maria) 239 | ) 240 | ) 241 | 242 | #DEF(#ANIMCONV(addr,x,y,fix)(frame) 243 | #LET(dir=#PEEK($addr+214)) 244 | #LET(pos=#PEEK($addr+215)+256*#PEEK($addr+216)-24064) 245 | #LET(length=#PEEK($addr+217)) 246 | #LET(x={pos}%32-$x) 247 | #LET(y={pos}/32-$y) 248 | #PUSHS 249 | #LET(cta=#IF($fix)($addr+205,#BTILEADDR($addr,#PEEK($addr+205)))) 250 | #LET(orig=#PEEK({cta}+1)+256*#PEEK({cta}+3)) 251 | #LET(done=0) 252 | #LET(lf=0) 253 | #FOR(0,2)(q, 254 | #IF({done}==0)( 255 | #IF({dir}==1)( 256 | #POKES({cta}+1,(#PEEK({cta}+1)>>2)+(#PEEK({cta}+1)&3)*64) 257 | #POKES({cta}+3,((#PEEK({cta}+3)<<2)&255)+(#PEEK({cta}+3)>>6)) 258 | ) 259 | #IF({dir}==0)( 260 | #POKES({cta}+1,((#PEEK({cta}+1)<<2)&255)+(#PEEK({cta}+1)>>6)) 261 | #POKES({cta}+3,(#PEEK({cta}+3)>>2)+(#PEEK({cta}+3)&3)*64) 262 | ) 263 | #LET(done={orig}==#PEEK({cta}+1)+256*#PEEK({cta}+3)) 264 | #IF({done}==0)( 265 | #TILE({cta})(ctile) 266 | #COPY($frame,$frame-q) 267 | #FOR({x},{x}+{length}-1)(z, 268 | #OVER(z,{y},,,2)($f)($frame-q,ctile) 269 | ) 270 | #LET(lf=q) 271 | ) 272 | ) 273 | ) 274 | #POPS 275 | ) 276 | 277 | #DEF(#ROOM(num,scale=2,x=0,y=0,w=32,h=17,flags=1)(fname) 278 | #REM( 279 | num: room number (0-60) 280 | scale: image scale 281 | x, y, w, h: geometry of frame 282 | flags: 283 | 1 - show items and guardians 284 | 2 - animate conveyor 285 | 4 - rebuild empty room (in case room definition has been modified) 286 | 8 - fix tile bug that affects conveyors and nasties (implies rebuild) 287 | ) 288 | #IF($flags&12)|| 289 | #RFRAME($num,1,$flags&8)(temp) 290 | #COPY($x,$y,$w,$h,$scale)(temp,room) 291 | | 292 | #RFRAME($num) 293 | #COPY($x,$y,$w,$h,$scale)($num,room) 294 | || 295 | #LET(addr=49152+256*$num) 296 | #IF($flags&1)( 297 | #ITEMS({addr},$x,$y)(room) 298 | #GUARDIANS({addr},$x,$y)(room) 299 | ) 300 | #IF($flags&2)|| 301 | #ANIMCONV({addr},$x,$y,$flags&8)(room) 302 | #FRAMES#((room,10#FOR(0,{lf})(n,;room-n)))({ScreenshotImagePath}/$fname) 303 | | 304 | #FRAMES(room)({ScreenshotImagePath}/$fname) 305 | || 306 | ) 307 | 308 | #DEF(#WILLY(rnum,x,y,sprite,left=0,top=0,width=32,height=17,force=0)(fname) 309 | #REM( 310 | rnum: room number (0-60) 311 | x, y: Willy's location in room 312 | sprite: Willy's sprite index (0-7) 313 | left, top, width, height: geometry of frame 314 | force: rebuild empty room (in case room definition has been modified) 315 | ) 316 | #IF($force)|| 317 | #RFRAME($rnum,1)(temp) 318 | #COPY($left,$top,$width,$height)(temp,room) 319 | | 320 | #RFRAME($rnum) 321 | #COPY($left,$top,$width,$height)($rnum,room) 322 | || 323 | #LET(addr=49152+256*$rnum) 324 | #SFRAME(40192+32*$sprite,7)(willy) 325 | #LET(bg=#PEEK({addr}+160)) 326 | #OVER($x-$left,0,0,$y-8*$top,1)(#IF($b=={bg})($b&56|$f&71,$b))(room,willy) 327 | #FRAMES(room)({ScreenshotImagePath}/$fname) 328 | ) 329 | 330 | #DEF(#GAMEOVER 331 | #FOR(255,63,-4,7)(e,[13*e+33]*63,13*e+13227+10*#EVAL(135<=e<=163)) 332 | ) 333 | 334 | [ThemeTune] 335 | [[[3246]*3, [3288, [3246]*2]*5, 3288]*4, [3246]*3, [3288, [3246]*2]*6, [3288, 336 | [3246]*3, [3288, [3246]*2]*5]*3, 3288, [3246]*3, 3288, [3246]*2, 3291, [6486, 337 | [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 338 | [6486, 6528, [6486, [6528]*2]*3]*2, 6486, 6528, [6486, [6528]*2]*2, 6486, 339 | 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, 6486, 340 | 356, [[2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, 341 | 2448]*2, [2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, 342 | [2448, [2406]*4, [2448, [2406]*3]*2]*2, 2451, 4806, [4848, [4806]*2, [4848, 343 | 4806]*7, 4848, [4806]*2, [4848, 4806]*6]*3, 4848, [4806]*2, [4848, 4806]*2, 344 | 3842, [[2046]*4, 2088]*50, [2046]*4, 2091, 2046, [[4086]*2, 4128, 4086, 345 | 4128]*24, [4086]*2, 2156]*7, [[3246]*3, [3288, [3246]*2]*5, 3288]*4, [3246]*3, 346 | [3288, [3246]*2]*6, [3288, [3246]*3, [3288, [3246]*2]*5]*3, 3288, [3246]*3, 347 | 3288, [3246]*2, 3291, [6486, [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 348 | 6486, 6528, [6486, [6528]*2]*2, [6486, 6528, [6486, [6528]*2]*3]*2, 6486, 349 | 6528, [6486, [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, 350 | [6528]*2]*2, 6486, 6528, 6486, 356, [[2406]*4, [2448, [2406]*3]*2, [2448, 351 | [2406]*4, [2448, [2406]*3]*3]*3, 2448]*2, [[2406]*4, [2448, [2406]*3]*2, 352 | [2448, [2406]*4, [2448, [2406]*3]*3]*3, [2448, [2406]*4, [2448, 353 | [2406]*3]*2]*2, 2451, 4806, [4848, [4806]*2, [4848, 4806]*7, 4848, [4806]*2, 354 | [4848, 4806]*6]*3, 4848, [4806]*2, [4848, 4806]*2, 3842, [[2046]*4, 2088]*50, 355 | [2046]*4, 2091, 2046, [[4086]*2, 4128, 4086, 4128]*24, [4086]*2, 2156, 356 | [[3046]*3, 3088, [3046]*2, [3088, [3046]*3, [3088, [3046]*2]*2]*2, 3088, 357 | [3046]*3, 3088, [3046]*2, [3088, [3046]*3, [3088, [3046]*2]*2]*3, 3088]*2, 358 | [3046]*3, 3088, [3046]*2, [3088, [3046]*3, [3088, [3046]*2]*2]*2, 3088, 359 | [3046]*3, 3088, [3046]*2, 3088, [3046]*3, [3088, [3046]*2]*2, 3091, [6086, 360 | 6128, 6086, [6128]*2]*2, [[6086, 6128]*2, [6086, [6128]*2, 6086, 6128]*5, 361 | 6086, [6128]*2]*2, [6086, 6128]*2, 6086, [6128]*2, 6086, 5162, [[2406]*3, 362 | 2448]*3, [[2406]*4, [2448, [2406]*3]*3, 2448]*3, [2406]*4, [2448, [2406]*3]*2, 363 | [2448, [2406]*4, [2448, [2406]*3]*3]*3, 2448]*2, [2406]*4, [2448, [2406]*3]*2, 364 | [2448, [2406]*4, [2448, [2406]*3]*3]*3, [2448, [2406]*4, [2448, 365 | [2406]*3]*2]*2, 2451, 4806, [4848, [4806]*2, [4848, 4806]*7, 4848, [4806]*2, 366 | [4848, 4806]*6]*3, 4848, [4806]*2, [4848, 4806]*2, 3842, [[2046]*4, 2088]*50, 367 | [2046]*4, 2091, 2046, [[4086]*2, 4128, 4086, 4128]*24, [4086]*2, 2156, 368 | [[[3046]*3, 3088, [3046]*2, [3088, [3046]*3, [3088, [3046]*2]*2]*2, 3088, 369 | [3046]*3, 3088, [3046]*2, [3088, [3046]*3, [3088, [3046]*2]*2]*3, 3088]*2, 370 | [3046]*3, 3088, [3046]*2, [3088, [3046]*3, [3088, [3046]*2]*2]*2, 3088, 371 | [3046]*3, 3088, [3046]*2, 3088, [3046]*3, [3088, [3046]*2]*2, 3091, [6086, 372 | 6128, 6086, [6128]*2]*2, [[6086, 6128]*2, [6086, [6128]*2, 6086, 6128]*5, 373 | 6086, [6128]*2]*2, [6086, 6128]*2, 6086, [6128]*2, 6086, 5042, [[2286]*3, 374 | 2328]*2, [[2286]*4, 2328, [2286]*3, 2328]*24, [2286]*4, 2331, 4566, 4608, 375 | [4566]*2, [4608, 4566]*4, [4608, [4566]*2, [4608, 4566]*3]*10, 4608, [4566]*2, 376 | [4608, 4566]*2, 2756, [[1806]*5, 1848]*3, [[1806]*4, [1848, [1806]*5]*2, 377 | 1848]*3, [[1806]*4, [1848, [1806]*5]*3, [1848, [1806]*4, [1848, 378 | [1806]*5]*2]*4, 1848]*2, [1806]*4, [1848, [1806]*5]*3, 1848, [1806]*4, [1848, 379 | [1806]*5]*2, 1851, [[3606]*2, 3648]*3, 3606, [3648, [3606]*2]*6, [3648, 3606, 380 | [3648, [3606]*2]*5]*2, [3648, 3606, [3648, [3606]*2]*6, 3648, 3606, [3648, 381 | [3606]*2]*5]*2, 3648, 3606, 3596]*2, [[3246]*3, [3288, [3246]*2]*5, 3288]*4, 382 | [3246]*3, [3288, [3246]*2]*6, [3288, [3246]*3, [3288, [3246]*2]*5]*3, 3288, 383 | [3246]*3, 3288, [3246]*2, 3291, [6486, [6528]*2]*2, 6486, 6528, [6486, 384 | [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, [6486, 6528, [6486, 385 | [6528]*2]*3]*2, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, [6486, 386 | [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, 6486, 356, [[2566]*3, 387 | 2608]*50, [2566]*3, 2611, 2566, [5126, 5168]*48, 5126, 4562, [[1806]*4, [1848, 388 | [1806]*5]*2, 1848]*4, [[1806]*4, [1848, [1806]*5]*3, [1848, [1806]*4, [1848, 389 | [1806]*5]*2]*4, 1848]*2, [1806]*4, [1848, [1806]*5]*3, 1848, [1806]*4, [1848, 390 | [1806]*5]*2, 1851, [[3606]*2, 3648]*3, 3606, [3648, [3606]*2]*6, [3648, 3606, 391 | [3648, [3606]*2]*5]*2, [3648, 3606, [3648, [3606]*2]*6, 3648, 3606, [3648, 392 | [3606]*2]*5]*2, 3648, 3606, 3596, [[3246]*3, [3288, [3246]*2]*5, 3288]*4, 393 | [3246]*3, [3288, [3246]*2]*6, [3288, [3246]*3, [3288, [3246]*2]*5]*3, 3288, 394 | [3246]*3, 3288, [3246]*2, 3291, [6486, [6528]*2]*2, 6486, 6528, [6486, 395 | [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, [6486, 6528, [6486, 396 | [6528]*2]*3]*2, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, [6486, 397 | [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, 6486, 356, [[2406]*4, 398 | [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, 2448]*2, [2406]*4, 399 | [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, [2448, [2406]*4, 400 | [2448, [2406]*3]*2]*2, 2451, 4806, [4848, [4806]*2, [4848, 4806]*7, 4848, 401 | [4806]*2, [4848, 4806]*6]*3, 4848, [4806]*2, [4848, 4806]*2, 3842, [[2046]*4, 402 | 2088]*50, [2046]*4, 2091, 2046, [[4086]*2, 4128, 4086, 4128]*24, [4086]*2, 403 | 2156, [[3246]*3, [3288, [3246]*2]*5, 3288]*4, [3246]*3, [3288, [3246]*2]*6, 404 | [3288, [3246]*3, [3288, [3246]*2]*5]*3, 3288, [3246]*3, 3288, [3246]*2, 3291, 405 | [6486, [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, 406 | [6528]*2]*2, [6486, 6528, [6486, [6528]*2]*3]*2, 6486, 6528, [6486, 407 | [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 408 | 6486, 6528, 6486, 356, [[2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, 409 | [2406]*3]*3]*3, 2448]*2, [2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, 410 | [2406]*3]*3]*3, [2448, [2406]*4, [2448, [2406]*3]*2]*2, 2451, 4806, [4848, 411 | [4806]*2, [4848, 4806]*7, 4848, [4806]*2, [4848, 4806]*6]*3, 4848, [4806]*2, 412 | [4848, 4806]*2, 3962, [2166]*3, [2208, [2166]*4]*2, [2208, [2166]*3, [2208, 413 | [2166]*4]*3]*5, 2208, [2166]*3, [2208, [2166]*4]*2, [2208, [2166]*3, [2208, 414 | [2166]*4]*3]*6, 2208, [2166]*3, 2211, [4326]*2, [4368, 4326]*2, [4368, 415 | [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*2]*2, 4368, [4326]*2, 416 | 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*3, [4368, [4326]*2, 4368, 4326, 417 | [4368, [4326]*2, [4368, 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, 4368, 418 | [4326]*2, 4002, [3646, [3688, [3646]*2]*4, 3688]*2, [3646, [3688, [3646]*2]*5, 419 | [3688, 3646, [3688, [3646]*2]*4]*2, 3688]*2, 3646, [3688, [3646]*2]*5, 3688, 420 | 3646, [3688, [3646]*2]*2, 3691, 7286, [7328]*3, 7286, [7328]*2, 7286, 421 | [7328]*3, [7286, [7328]*2]*2, [7286, [7328]*3, 7286, [7328]*2]*5, 7286, 422 | [7328]*3, [7286, [7328]*2]*2, 7286, [7328]*3, 7286, 7328, 7562, [2566]*2, 423 | [2608, [2566]*3]*50, 2611, 2566, [5126, 5168]*48, 5126, 4922, [2166]*3, [2208, 424 | [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*5, 2208, [2166]*3, [2208, 425 | [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*6, 2208, [2166]*3, 2211, 426 | [4326]*2, [4368, 4326]*2, [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 427 | 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*3, 428 | [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*2]*2, 4368, 429 | [4326]*2, 4368, 4326, 4368, [4326]*2, 4442, [4086, 4128, [4086]*2, 4128]*25, 430 | 4086, 4131, 4086, 8208, [8166, [8208]*4]*11, 8166, [8208]*3, 8276, [[3246]*3, 431 | [3288, [3246]*2]*5, 3288]*4, [3246]*3, [3288, [3246]*2]*6, [3288, [3246]*3, 432 | [3288, [3246]*2]*5]*3, 3288, [3246]*3, 3288, [3246]*2, 3291, [6486, 433 | [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 434 | [6486, 6528, [6486, [6528]*2]*3]*2, 6486, 6528, [6486, [6528]*2]*2, 6486, 435 | 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, 6486, 436 | 356, [[2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, 437 | 2448]*2, [2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, 438 | [2448, [2406]*4, [2448, [2406]*3]*2]*2, 2451, 4806, [4848, [4806]*2, [4848, 439 | 4806]*7, 4848, [4806]*2, [4848, 4806]*6]*3, 4848, [4806]*2, [4848, 4806]*2, 440 | 5042, [[3246]*2, 3288]*6, [[3246]*3, [3288, [3246]*2]*5, 3288]*3, [3246]*3, 441 | [3288, [3246]*2]*6, [3288, [3246]*3, [3288, [3246]*2]*5]*3, 3288, [3246]*3, 442 | 3288, [3246]*2, 3291, [6486, [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 443 | 6486, 6528, [6486, [6528]*2]*2, [6486, 6528, [6486, [6528]*2]*3]*2, 6486, 444 | 6528, [6486, [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, 445 | [6528]*2]*2, 6486, 6528, 6486, 356, [[2406]*4, [2448, [2406]*3]*2, [2448, 446 | [2406]*4, [2448, [2406]*3]*3]*3, 2448]*2, [2406]*4, [2448, [2406]*3]*2, [2448, 447 | [2406]*4, [2448, [2406]*3]*3]*3, [2448, [2406]*4, [2448, [2406]*3]*2]*2, 2451, 448 | 4806, [4848, [4806]*2, [4848, 4806]*7, 4848, [4806]*2, [4848, 4806]*6]*3, 449 | 4848, [4806]*2, [4848, 4806]*2, 3842, [[2046]*4, 2088]*50, [2046]*4, 2091, 450 | 2046, [[4086]*2, 4128, 4086, 4128]*24, [4086]*2, 2156, [[3246]*3, [3288, 451 | [3246]*2]*5, 3288]*4, [3246]*3, [3288, [3246]*2]*6, [3288, [3246]*3, [3288, 452 | [3246]*2]*5]*3, 3288, [3246]*3, 3288, [3246]*2, 3291, [6486, [6528]*2]*2, 453 | 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, [6486, 6528, 454 | [6486, [6528]*2]*3]*2, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, [6486, 455 | [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, 6486, 356, [[2406]*4, 456 | [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, 2448]*2, [2406]*4, 457 | [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, [2448, [2406]*4, 458 | [2448, [2406]*3]*2]*2, 2451, 4806, [4848, [4806]*2, [4848, 4806]*7, 4848, 459 | [4806]*2, [4848, 4806]*6]*3, 4848, [4806]*2, [4848, 4806]*2, 3842, [[2046]*4, 460 | 2088]*50, [2046]*4, 2091, 2046, [[4086]*2, 4128, 4086, 4128]*24, [4086]*2, 461 | 2156, [[1606]*6, 1648, [1606]*5, 1648, [1606]*6, [1648, [1606]*5]*2, 1648]*10, 462 | [1606]*6, 1651, [3206]*2, [3248, [3206]*3, [3248, [3206]*2]*4]*9, 3248, 463 | [3206]*3, [3248, [3206]*2]*2, 1796, [[2406]*4, [2448, [2406]*3]*2, [2448, 464 | [2406]*4, [2448, [2406]*3]*3]*3, 2448]*2, [2406]*4, [2448, [2406]*3]*2, [2448, 465 | [2406]*4, [2448, [2406]*3]*3]*3, [2448, [2406]*4, [2448, [2406]*3]*2]*2, 2451, 466 | 4806, [4848, [4806]*2, [4848, 4806]*7, 4848, [4806]*2, [4848, 4806]*6]*3, 467 | 4848, [4806]*2, [4848, 4806]*2, 3402, [[1606]*5, 1648]*2, [[1606]*6, [1648, 468 | [1606]*5]*2, 1648, [1606]*6, 1648, [1606]*5, 1648]*9, [1606]*6, [1648, 469 | [1606]*5]*2, 1648, [1606]*6, 1651, [3206]*2, [3248, [3206]*3, [3248, 470 | [3206]*2]*4]*9, 3248, [3206]*3, [3248, [3206]*2]*2, 1796, [[1606]*6, 1648, 471 | [1606]*5, 1648, [1606]*6, [1648, [1606]*5]*2, 1648]*10, [1606]*6, 1651, 472 | [3206]*2, [3248, [3206]*3, [3248, [3206]*2]*4]*9, 3248, [3206]*3, [3248, 473 | [3206]*2]*2, 1796, [[[2166]*4, 2208]*3, [[2166]*3, [2208, [2166]*4]*3, 474 | 2208]*5, [2166]*3, [2208, [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*6, 475 | 2208, [2166]*3, 2211, [4326]*2, [4368, 4326]*2, [4368, [4326]*2, 4368, 4326, 476 | [4368, [4326]*2, [4368, 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, [4368, 477 | [4326]*2, [4368, 4326]*2]*3, [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, 478 | [4368, 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, 4368, [4326]*2, 2162, 479 | [[1806]*4, [1848, [1806]*5]*2, 1848]*4, [[1806]*4, [1848, [1806]*5]*3, [1848, 480 | [1806]*4, [1848, [1806]*5]*2]*4, 1848]*2, [1806]*4, [1848, [1806]*5]*3, 1848, 481 | [1806]*4, [1848, [1806]*5]*2, 1851, [[3606]*2, 3648]*3, 3606, [3648, 482 | [3606]*2]*6, [3648, 3606, [3648, [3606]*2]*5]*2, [3648, 3606, [3648, 483 | [3606]*2]*6, 3648, 3606, [3648, [3606]*2]*5]*2, 3648, 3606, 3596, [[3246]*3, 484 | [3288, [3246]*2]*5, 3288]*4, [3246]*3, [3288, [3246]*2]*6, [3288, [3246]*3, 485 | [3288, [3246]*2]*5]*3, 3288, [3246]*3, 3288, [3246]*2, 3291, [6486, 486 | [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 487 | [6486, 6528, [6486, [6528]*2]*3]*2, 6486, 6528, [6486, [6528]*2]*2, 6486, 488 | 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, 6486, 489 | 356]*2, [[2166]*4, 2208]*3, [[2166]*3, [2208, [2166]*4]*3, 2208]*5, [2166]*3, 490 | [2208, [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*6, 2208, [2166]*3, 491 | 2211, [4326]*2, [4368, 4326]*2, [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, 492 | [4368, 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 493 | 4326]*2]*3, [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 494 | 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, 4368, [4326]*2, 2162, [[1806]*4, 495 | [1848, [1806]*5]*2, 1848]*4, [[1806]*4, [1848, [1806]*5]*3, [1848, [1806]*4, 496 | [1848, [1806]*5]*2]*4, 1848]*2, [1806]*4, [1848, [1806]*5]*3, 1848, [1806]*4, 497 | [1848, [1806]*5]*2, 1851, [[3606]*2, 3648]*3, 3606, [3648, [3606]*2]*6, [3648, 498 | 3606, [3648, [3606]*2]*5]*2, [3648, 3606, [3648, [3606]*2]*6, 3648, 3606, 499 | [3648, [3606]*2]*5]*2, 3648, 3606, 3596, [[1606]*6, 1648, [1606]*5, 1648, 500 | [1606]*6, [1648, [1606]*5]*2, 1648]*10, [1606]*6, 1651, [3206]*2, [3248, 501 | [3206]*3, [3248, [3206]*2]*4]*9, 3248, [3206]*3, [3248, [3206]*2]*2, 1796, 502 | [[2166]*4, 2208]*3, [[2166]*3, [2208, [2166]*4]*3, 2208]*5, [2166]*3, [2208, 503 | [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*6, 2208, [2166]*3, 2211, 504 | [4326]*2, [4368, 4326]*2, [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 505 | 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*3, 506 | [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*2]*2, 4368, 507 | [4326]*2, 4368, 4326, 4368, [4326]*2, 1962, [[1606]*5, 1648]*2, [[1606]*6, 508 | [1648, [1606]*5]*2, 1648, [1606]*6, 1648, [1606]*5, 1648]*9, [1606]*6, [1648, 509 | [1606]*5]*2, 1648, [1606]*6, 1651, [3206]*2, [3248, [3206]*3, [3248, 510 | [3206]*2]*4]*9, 3248, [3206]*3, [3248, [3206]*2]*2, 1796, [[1606]*6, 1648, 511 | [1606]*5, 1648, [1606]*6, [1648, [1606]*5]*2, 1648]*10, [1606]*6, 1651, 512 | [3206]*2, [3248, [3206]*3, [3248, [3206]*2]*4]*9, 3248, [3206]*3, [3248, 513 | [3206]*2]*2, 1796, [[2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, 514 | [2406]*3]*3]*3, 2448]*2, [2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, 515 | [2406]*3]*3]*3, [2448, [2406]*4, [2448, [2406]*3]*2]*2, 2451, 4806, [4848, 516 | [4806]*2, [4848, 4806]*7, 4848, [4806]*2, [4848, 4806]*6]*3, 4848, [4806]*2, 517 | [4848, 4806]*2, 3842, [[2046]*4, 2088]*50, [2046]*4, 2091, 2046, [[4086]*2, 518 | 4128, 4086, 4128]*24, [4086]*2, 2156, [[3246]*3, [3288, [3246]*2]*5, 3288]*4, 519 | [3246]*3, [3288, [3246]*2]*6, [3288, [3246]*3, [3288, [3246]*2]*5]*3, 3288, 520 | [3246]*3, 3288, [3246]*2, 3291, [6486, [6528]*2]*2, 6486, 6528, [6486, 521 | [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, [6486, 6528, [6486, 522 | [6528]*2]*3]*2, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, [6486, 523 | [6528]*2]*3, 6486, 6528, [6486, [6528]*2]*2, 6486, 6528, 6486, 356, [[2406]*4, 524 | [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, 2448]*2, [2406]*4, 525 | [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, [2448, [2406]*4, 526 | [2448, [2406]*3]*2]*2, 2451, 4806, [4848, [4806]*2, [4848, 4806]*7, 4848, 527 | [4806]*2, [4848, 4806]*6]*3, 4848, [4806]*2, [4848, 4806]*2, 3842, [[2046]*4, 528 | 2088]*50, [2046]*4, 2091, 2046, [[4086]*2, 4128, 4086, 4128]*24, [4086]*2, 529 | 2156, [[1526]*6, 1568]*3, [[1526]*5, [1568, [1526]*6]*3, 1568]*3, [1526]*5, 530 | [1568, [1526]*6]*2, [1568, [1526]*5, [1568, [1526]*6]*3]*4, 1568, [1526]*5, 531 | [1568, [1526]*6]*2, [1568, [1526]*5, [1568, [1526]*6]*3]*3, 1568, [1526]*5, 532 | 1568, [1526]*6, 1571, [[3046]*3, [3088, [3046]*2]*2, 3088]*2, [[3046]*3, 3088, 533 | [3046]*2, [3088, [3046]*3, [3088, [3046]*2]*2]*2, 3088, [3046]*3, 3088, 534 | [3046]*2, [3088, [3046]*3, [3088, [3046]*2]*2]*3, 3088]*2, [3046]*3, 3088, 535 | [3046]*2, 3088, [3046]*3, [3088, [3046]*2]*2, 5162, [[2406]*3, 2448]*3, 536 | [[2406]*4, [2448, [2406]*3]*3, 2448]*3, [2406]*4, [2448, [2406]*3]*2, [2448, 537 | [2406]*4, [2448, [2406]*3]*3]*3, 2448, [2406]*4, [2448, [2406]*3]*2, [2448, 538 | [2406]*4, [2448, [2406]*3]*3]*3, [2448, [2406]*4, [2448, [2406]*3]*2]*2, 2451, 539 | 4806, [4848, [4806]*2, [4848, 4806]*7, 4848, [4806]*2, [4848, 4806]*6]*3, 540 | 4848, [4806]*2, [4848, 4806]*2, 3602, [[1806]*4, [1848, [1806]*5]*2, 1848]*4, 541 | [[1806]*4, [1848, [1806]*5]*3, [1848, [1806]*4, [1848, [1806]*5]*2]*4, 542 | 1848]*2, [1806]*4, [1848, [1806]*5]*3, 1848, [1806]*4, [1848, [1806]*5]*2, 543 | 1851, [[3606]*2, 3648]*3, 3606, [3648, [3606]*2]*6, [3648, 3606, [3648, 544 | [3606]*2]*5]*2, [3648, 3606, [3648, [3606]*2]*6, 3648, 3606, [3648, 545 | [3606]*2]*5]*2, 3648, 3606, 3596, [[3046]*3, 3088, [3046]*2, [3088, [3046]*3, 546 | [3088, [3046]*2]*2]*2, 3088, [3046]*3, 3088, [3046]*2, [3088, [3046]*3, [3088, 547 | [3046]*2]*2]*3, 3088]*2, [3046]*3, 3088, [3046]*2, [3088, [3046]*3, [3088, 548 | [3046]*2]*2]*2, 3088, [3046]*3, 3088, [3046]*2, 3088, [3046]*3, [3088, 549 | [3046]*2]*2, 3091, [6086, 6128, 6086, [6128]*2]*2, [[6086, 6128]*2, [6086, 550 | [6128]*2, 6086, 6128]*5, 6086, [6128]*2]*2, [6086, 6128]*2, 6086, [6128]*2, 551 | 6086, 5162, [[2406]*3, 2448]*3, [[2406]*4, [2448, [2406]*3]*3, 2448]*3, 552 | [2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, 2448, 553 | [2406]*4, [2448, [2406]*3]*2, [2448, [2406]*4, [2448, [2406]*3]*3]*3, [2448, 554 | [2406]*4, [2448, [2406]*3]*2]*2, 2451, 4806, [4848, [4806]*2, [4848, 4806]*7, 555 | 4848, [4806]*2, [4848, 4806]*6]*3, 4848, [4806]*2, [4848, 4806]*2, 3602, 556 | [[1806]*4, [1848, [1806]*5]*2, 1848]*4, [[1806]*4, [1848, [1806]*5]*3, [1848, 557 | [1806]*4, [1848, [1806]*5]*2]*4, 1848]*2, [1806]*4, [1848, [1806]*5]*3, 1848, 558 | [1806]*4, [1848, [1806]*5]*2, 1851, [[3606]*2, 3648]*3, 3606, [3648, 559 | [3606]*2]*6, [3648, 3606, [3648, [3606]*2]*5]*2, [3648, 3606, [3648, 560 | [3606]*2]*6, 3648, 3606, [3648, [3606]*2]*5]*2, 3648, 3606, 3596, [[1606]*6, 561 | 1648, [1606]*5, 1648, [1606]*6, [1648, [1606]*5]*2, 1648]*10, [1606]*6, 1651, 562 | [3206]*2, [3248, [3206]*3, [3248, [3206]*2]*4]*9, 3248, [3206]*3, [3248, 563 | [3206]*2]*2, 1796, [[2566]*3, 2608]*50, [2566]*3, 2611, 2566, [5126, 5168]*48, 564 | 5126, 4802, [[2046]*4, 2088]*50, [2046]*4, 2091, 2046, [[4086]*2, 4128, 4086, 565 | 4128]*24, [4086]*2, 2156, [[3246]*3, [3288, [3246]*2]*5, 3288]*4, [3246]*3, 566 | [3288, [3246]*2]*6, [3288, [3246]*3, [3288, [3246]*2]*5]*3, 3288, [3246]*3, 567 | 3288, [3246]*2, 3291, [6486, [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 568 | 6486, 6528, [6486, [6528]*2]*2, [6486, 6528, [6486, [6528]*2]*3]*2, 6486, 569 | 6528, [6486, [6528]*2]*2, 6486, 6528, [6486, [6528]*2]*3, 6486, 6528, [6486, 570 | [6528]*2]*2, 6486, 6528, 6486, 356, [[2566]*3, 2608]*50, [2566]*3, 2611, 2566, 571 | [5126, 5168]*48, 5126, 4802, [[2046]*4, 2088]*50, [2046]*4, 2091, 2046, 572 | [[4086]*2, 4128, 4086, 4128]*24, [4086]*2, 2156, [[1806]*5, 1848]*3, 573 | [[1806]*4, [1848, [1806]*5]*2, 1848]*3, [[1806]*4, [1848, [1806]*5]*3, [1848, 574 | [1806]*4, [1848, [1806]*5]*2]*4, 1848]*2, [1806]*4, [1848, [1806]*5]*3, 1848, 575 | [1806]*4, [1848, [1806]*5]*2, 1851, [[3606]*2, 3648]*3, 3606, [3648, 576 | [3606]*2]*6, [3648, 3606, [3648, [3606]*2]*5]*2, [3648, 3606, [3648, 577 | [3606]*2]*6, 3648, 3606, [3648, [3606]*2]*5]*2, 3648, 3606, 3596, [[2566]*3, 578 | 2608]*50, [2566]*3, 2611, 2566, [5126, 5168]*48, 5126, 4922, [2166]*3, [2208, 579 | [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*5, 2208, [2166]*3, [2208, 580 | [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*6, 2208, [2166]*3, 2211, 581 | [4326]*2, [4368, 4326]*2, [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 582 | 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*3, 583 | [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*2]*2, 4368, 584 | [4326]*2, 4368, 4326, 4368, [4326]*2, 1642, [1286]*6, [1328, [1286]*7]*50, 585 | 1331, 1286, [[2566]*3, 2608]*48, [2566]*3, 4042, [2566]*2, [2608, 586 | [2566]*3]*50, 2611, 2566, [5126, 5168]*48, 5126, 4922, [2166]*3, [2208, 587 | [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*5, 2208, [2166]*3, [2208, 588 | [2166]*4]*2, [2208, [2166]*3, [2208, [2166]*4]*3]*6, 2208, [2166]*3, 2211, 589 | [4326]*2, [4368, 4326]*2, [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 590 | 4326]*2]*2]*2, 4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*3, 591 | [4368, [4326]*2, 4368, 4326, [4368, [4326]*2, [4368, 4326]*2]*2]*2, 4368, 592 | [4326]*2, 4368, 4326, 4368, [4326]*2, 2802, [[2446]*3, 2488]*5, [[2446]*4, 593 | [2488, [2446]*3]*4, 2488]*9, [2446]*4, 2491, [4886, 4928]*4, [4886]*2, [4928, 594 | 4886]*10, [4928, [4886]*2, [4928, 4886]*9]*3, 4928, [4886]*2, [4928, 4886]*3, 595 | 1836, [4846]*2, [4888, 4846]*7, 4888, [4846]*2, [4888, 4846]*8, 4888, 596 | [4846]*2, [4888, 4846]*7, [4888, [4846]*2, [4888, 4846]*8]*2, 4888, [4846]*2, 597 | [4888, 4846]*7, 4891, 9686, [9728]*18, 9686, [9728]*17, 9686, [9728]*13, 7796, 598 | [[2446]*4, [2488, [2446]*3]*4, 2488]*10, [2446]*4, 2491, [4886, 4928]*4, 599 | [4886]*2, [4928, 4886]*10, [4928, [4886]*2, [4928, 4886]*9]*3, 4928, [4886]*2, 600 | [4928, 4886]*3 601 | 602 | [InGameTune] 603 | [3446, [3457, [3446]*2]*2, 290000]*2, 3846, 3857, [3846]*2, 3857, 3846, 604 | 290000, [[3846]*2, 3857]*2, 3846, 290000, [[3446]*2, 3457]*2, [3446]*2, 605 | 290000, 3446, [3457, [3446]*2]*2, 290000, 3846, 3857, [3846]*2, 3857, 3846, 606 | 290000, [[3846]*2, 3857]*2, 3846, [290000, [[4086]*2, 4097]*2, 4086]*4, 607 | [290000, [5126, 5137]*2, 5126]*8, [290000, [[4086]*2, 4097]*2, 4086]*2, 608 | [290000, [[3846]*2, 3857]*2, 3846]*2, [290000, [[3446]*2, 3457]*2, [3446]*2, 609 | 290000, 3446, [3457, [3446]*2]*2, 290000, 3846, 3857, [3846]*2, 3857, 3846, 610 | 290000, [[3846]*2, 3857]*2, 3846]*2, [290000, [[4086]*2, 4097]*2, 4086]*2, 611 | [290000, [[3846]*2, 3857]*2, 3846]*2, 290000, [[3446]*2, 3457]*2, [3446]*2, 612 | 290000, 3446, [3457, [3446]*2]*2, [290000, [[3046]*2, 3057]*2, [3046]*3]*2, 613 | [290000, [2886]*2, 2897, [2886]*3, 2897, [2886]*2]*2, [290000, [[3046]*2, 614 | 3057]*2, [3046]*3]*2, [290000, [2886]*2, 2897, [2886]*3, 2897, [2886]*2]*2, 615 | [290000, [[3046]*2, 3057]*2, [3046]*3]*2, [290000, 3446, [3457, 616 | [3446]*2]*2]*16, 290000, [2566]*2, [2577, [2566]*3]*2, [290000, [[2566]*3, 617 | 2577]*2, [2566]*3]*7, [290000, [[2726]*3, 2737]*2, [2726]*3]*4, 290000, 618 | [3046]*3, 3057, [3046]*2, 3057, [[3046]*3, 290000, [[3046]*2, 3057]*2]*3, 619 | [3046]*3, [290000, 3446, [3457, [3446]*2]*2]*2, 290000, 3846, 3857, [3846]*2, 620 | 3857, 3846, 290000, [[3846]*2, 3857]*2, 3846, [290000, [[4086]*2, 4097]*2, 621 | 4086]*2, [290000, [[3846]*2, 3857]*2, 3846]*2, 290000, [[3446]*2, 3457]*2, 622 | [3446]*2, [290000, 3446, [3457, [3446]*2]*2]*3, 290000, 4086, 4097, [4086]*2, 623 | 4097, [4086, 290000, [[4086]*2, 4097]*2]*3, 4086, [290000, [3246]*3, [3257, 624 | [3246]*2]*2]*2, 290000, [[3446]*2, 3457]*2, [3446]*2, 290000, 3446, [3457, 625 | [3446]*2]*2, 290000, 3846, 3857, [3846]*2, 3857, 3846, 290000, [[3846]*2, 626 | 3857]*2, 3846, 290000, [[3446]*2, 3457]*2, [3446]*2, 290000, 3446, [3457, 627 | [3446]*2]*2, 290000, [[3246]*2, 3257]*2, [3246]*2, [290000, [3246]*3, [3257, 628 | [3246]*2]*2]*3, [290000, [[3846]*2, 3857]*2, 3846]*4, [290000, [[2566]*3, 629 | 2577]*2, [2566]*3]*16 630 | --------------------------------------------------------------------------------