├── 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 |
--------------------------------------------------------------------------------