v1.0
|
@ -0,0 +1,6 @@
|
|||
*.o
|
||||
*.dbg
|
||||
*.deb
|
||||
*.nes
|
||||
*.prg
|
||||
*.bin
|
|
@ -0,0 +1,232 @@
|
|||
# Overview
|
||||
This repository contains an annotated disassembly of the _Contra_ (US) NES ROM
|
||||
and the build script(s) to reassemble the assembly into a byte-for-byte match of
|
||||
the game. This repo also contains supplemental documentation, diagrams,
|
||||
scripts, and tools to further understand the game.
|
||||
|
||||
A special thanks goes to Trax. This project would not have gotten started
|
||||
without the amazing initial disassembly by him for his [Revenge of the Red
|
||||
Falcon](https://www.romhacking.net/hacks/2701/) project.
|
||||
|
||||
```
|
||||
|-- docs - supplemental documentation
|
||||
| |-- attachments - files used in other documentation
|
||||
| |-- diagrams - mermaid diagram files documenting program flow
|
||||
| |-- lua_scripts - lua scripts for mesen and fceux
|
||||
| |-- sprite_library - extracted sprites for ease of viewing
|
||||
|-- src - the source code for the game
|
||||
| |-- assets - the compressed graphics data and encoded audio for the game
|
||||
|-- assets.txt - a list of assets, their offset in baserom.nes and their length
|
||||
|-- build.bat - build script for Windows cmd (no PowerShell)
|
||||
|-- build.ps1 - recommended build script for Windows (PowerShell)
|
||||
|-- build.sh - bash build script for Linux/mac
|
||||
|-- contra.cfg - memory mapping of PRG banks used when building
|
||||
|-- README.md
|
||||
|-- set_bytes.vbs - script used by build.bat to extract data from baserom.nes
|
||||
```
|
||||
# Building
|
||||
|
||||
## Prerequisites
|
||||
* This repo does not include all assets (graphics data and audio data)
|
||||
necessary for assembling the ROM. An existing copy of the game is required.
|
||||
Place a copy of the US NES version of _Contra_ with the name `baserom.nes` in
|
||||
the project root folder. The file will be used by the build script to extract
|
||||
the necessary assets. The MD5 hash of `baserom.nes` ROM should be
|
||||
`7BDAD8B4A7A56A634C9649D20BD3011B`.
|
||||
* Building requires the [cc65 compiler suite](https://cc65.github.io/) to
|
||||
assemble and link the 6502 assembly files. Please install it and ensure the
|
||||
bin folder is added to your path.
|
||||
|
||||
## Instructions
|
||||
There are 3 build scripts in this repository. All of them do the same thing. To
|
||||
build the resulting .nes rom file, simply execute the appropriate build script
|
||||
based on your environment.
|
||||
|
||||
```
|
||||
.\build.ps1 <-- Windows
|
||||
.\build.bat <-- Windows (no PowerShell)
|
||||
./build.sh <-- Unix
|
||||
```
|
||||
|
||||
* `build.ps1` - PowerShell script recommended for building on Windows machines.
|
||||
* `build.bat` - bat script that can be used on windows machines without
|
||||
PowerShell, but requires VBScript support.
|
||||
* `build.sh` - bash script to be used in unix environments, or on Windows
|
||||
environments with bash support (Git bash, WSL, etc.)
|
||||
|
||||
## Documentation
|
||||
|
||||
Supplemental materials have been added that help explain interesting features of
|
||||
the code. Below are some of the more important documents.
|
||||
|
||||
* `docs/Aim Documentation.md` - documentation on how enemy aiming works
|
||||
* `docs/Bugs.md` - bugs identified while disassembling
|
||||
* `docs/Contra Control Flow.md` - detailed look at the game routine and level
|
||||
routine flows.
|
||||
* `docs/Enemy Glossary.md` - documentation on every enemy type in the game
|
||||
* `docs/Enemy Routines.md` - documentation on level enemy configuration and
|
||||
random soldier generation.
|
||||
* `docs/Graphics Documentation.md` - documentation on pattern tables,
|
||||
nametables, palettes, palette cycling, super-tiles, background collision, and
|
||||
compression.
|
||||
* `docs/Sound Documentation.md` - documentation on the audio engine used as well
|
||||
as information on all sounds from the game.
|
||||
|
||||
All sprites were captured and labeled for easy reference in
|
||||
`docs/sprite_library/README.md`
|
||||
|
||||
### Getting Started
|
||||
|
||||
At first, there is a lot to take in and it may be confusing where to start.
|
||||
The files in the repo under the `src/` folder are broken up based on the memory
|
||||
banks in the game. The _Contra_ cartridge is a
|
||||
[UxROM](https://www.nesdev.org/wiki/UxROM) cartridge. The game has 8, 16 KiB
|
||||
banks of memory for a total size of 128 KiB. Banks 0 through 6 are swapped in
|
||||
and out of memory at address $8000-$bfff and bank 7 is always in memory at
|
||||
$c000-$ffff. Knowing this helps understand the code since only 2 banks are
|
||||
available at any given time. The start of the game and most of the game engine
|
||||
logic exists in `bank7.asm` since that's in memory all of the time and where
|
||||
the 3 NES interrupts are.
|
||||
|
||||
I think a good way to start reading this code is to look into
|
||||
`docs/Contra Control Flow.md`. That file shows the responsibilities of the game
|
||||
engine loop routines and in what order those routines are called.
|
||||
|
||||
* `bank0.asm` - used exclusively for enemy routines. Enemy routines are the
|
||||
logic controlling enemy behaviors and settings: AI, movements, attack
|
||||
patterns, health, etc. Almost every enemy is coded in bank 0, but some
|
||||
enemy routines, usually those who appear in more than one level, are in
|
||||
`bank7.asm`.
|
||||
* `bank1.asm` - responsible for audio and sprites. The audio code takes up
|
||||
about 3/4 of the bank. The remaining 1/4 of the bank is for sprite data and
|
||||
code to draw sprites.
|
||||
* `bank2.asm` - starts with RLE-encoded level data (graphic super tiles for the
|
||||
level screens). It then contains compressed tile data and alternate tile data
|
||||
and occasional attribute table data. Then, bank 2 contains logic for setting
|
||||
the players' sprite based on player state. Next, bank 2 contains the level
|
||||
headers, which define specifics about each level. Bank 2 then has the data
|
||||
that specifies which enemies are on which screen and their attributes. Bank
|
||||
2 also contains the soldier enemy generation code.
|
||||
* `bank3.asm` - starts with the data that specifies which pattern table tiles
|
||||
comprises super-tiles along with the color palettes. This bank also has the
|
||||
routines to manage the end of levels.
|
||||
* `bank4.asm` - mostly contains compressed graphic data. The rest of bank 4 is
|
||||
the code for the ending scene animation and the ending credits, including the
|
||||
ending credits text data.
|
||||
* `bank5.asm` - mostly contains compressed graphic data. The rest of bank 5 is
|
||||
the code and lookup tables for automated input for the 3 demo (attract)
|
||||
levels.
|
||||
* `bank6.asm` - contains compressed graphics data, data for short text sequences
|
||||
like level names and menu options. Bank 6 also contains the code for the
|
||||
players' weapons and bullets.
|
||||
* `bank7.asm` - the core of the game's programming. Reset, NMI, and IRQ vectors
|
||||
are in this bank and is the entry point to the game. Bank 7 contains the code
|
||||
for drawing of nametables and sprites, bank switching, routines for the intro
|
||||
sequence, controller input, score calculation, graphics decompression
|
||||
routines, palette codes, collision detection, pointer table for enemy
|
||||
routines, shared enemy logic, score table, enemy attributes, and bullet angles
|
||||
and speeds, and the NES undocumented footer, among other things.
|
||||
|
||||
### Annotations
|
||||
|
||||
While reviewing the assembly, I used a notation to categorize unexpected, or
|
||||
interesting observations. You can search the code for these annotations.
|
||||
|
||||
* `!(BUG?)` - a possible bug
|
||||
* `!(HUH)` - unexpected code or logic used
|
||||
* `!(WHY?)` - unsure of why the code is the way it is
|
||||
* `!(UNUSED)` - unused code or data that is never executed nor read
|
||||
* `!(OBS)` - observation or note
|
||||
|
||||
# Project History
|
||||
I first became interested in this project after watching the
|
||||
[Summoning Salt](https://www.youtube.com/channel/UCtUbO6rBht0daVIOGML3c8w) video
|
||||
[The History of Contra World Records](https://www.youtube.com/watch?v=GgOE64kgjjo).
|
||||
In the video starting at 35m 19s, there was a section where a speedrunner named
|
||||
`dk28` died in the middle of level 1, and was advanced to level 2 for their next
|
||||
life. The video explains that there still isn't a known explanation as to why
|
||||
this happened. This got me interested in the assembly and thus this project
|
||||
began and has since changed in scope as my interest have changed.
|
||||
|
||||
The _Contra_ (US) NES ROM was disassembled using a NES disassembly tool. This
|
||||
provided a starting point for code investigation. The disassembler incorrectly
|
||||
marked many lines of code as data and this had to be manually corrected. At
|
||||
this point, there was an .asm file for each ROM bank in _Contra_.
|
||||
|
||||
Build scripts were then created for both windows console (.bat), and powershell
|
||||
(.ps1). These scripts are equivalent and use ca65 to assemble the .asm files
|
||||
and cl65 to link the .o files into a single ROM file. Creating these build
|
||||
scripts required defining the memory addresses and bank layouts in the
|
||||
`contra.cfg` file.
|
||||
|
||||
At this point, I had a repository that could be built that matched the NES rom
|
||||
byte for byte. Then, I found and was able to incorporate the comments from
|
||||
Trax's IDA Pro disassembly. This was incredibly helpful, but more work had to
|
||||
be done as Trax's disassembly wasn't appropriate for disassembly. For instance,
|
||||
it had a label for every line and all jump and branch statements were
|
||||
hard-coded memory address offsets and not label references.
|
||||
|
||||
I then worked on updating all branch and jump offsets to point to label
|
||||
addresses. This helped ensure that the code was more readable. At this point,
|
||||
I also started updating data blocks that included memory offsets to use label
|
||||
offsets. The goal here was twofold:
|
||||
|
||||
* make the code more readable and similar to what the original developers would
|
||||
have written
|
||||
* allow rom hacking without breaking the entire build due to breaking
|
||||
hard-coded memory offsets
|
||||
|
||||
When all of this was done, the project could assemble and be byte-for-byte
|
||||
exactly as the _Contra_ (US) ROM. However, this was just the prerequisite to
|
||||
documenting the codebase. Every label had to be given an appropriate name, and
|
||||
each line of assembly had to be documented.
|
||||
|
||||
# Build Details
|
||||
|
||||
The build scripts accomplish the following tasks:
|
||||
* extracts necessary data from `baserom.nes` into `src/assets`
|
||||
* assemble each bank .asm file into a .o file
|
||||
* assemble constants.asm and ines_header into .o files
|
||||
* link all output .o files into a single .nes rom file
|
||||
|
||||
The build scripts all utilize [cc65 compiler suite](https://cc65.github.io/) to
|
||||
assemble and link the 6502 assembly files. The asset data is pulled from
|
||||
`baserom.nes` as specified in `assets.txt`. Each line in `assets.txt` specifies
|
||||
the file name of the asset, its offset into `baserom.nes`, and its length.
|
||||
|
||||
## 1. Assembly
|
||||
Each .asm file is assembled into a .o object file. This section outlines what
|
||||
is happening as part of that process.
|
||||
|
||||
### Import Export Validation
|
||||
During assembly, if any symbol is undefined and not explicitly defined in an
|
||||
`.import` directive, an error will be generated.
|
||||
|
||||
## 2. Linking
|
||||
Linking is the concept of combining the various .o object files into a single
|
||||
.nes rom file. This is done by the cl65 linker. Its job is to replace labels
|
||||
with actual CPU memory addresses where the labels will exist when loaded. These
|
||||
are then stored in the resulting contra.nes rom file. This is aided by the
|
||||
linker configuration file contra.cfg.
|
||||
|
||||
This file specifies the layout of the .net rom file. Without this file, or with
|
||||
a misconfigured file, the linker would not generate an identical .nes file.
|
||||
There are two parts to the contra.cfg file: MEMORY layout and SEGMENTS layout.
|
||||
|
||||
### Contra.cfg MEMORY Layout
|
||||
This section tells the linker where in memory each bank will exist. The linker
|
||||
needs this to know to replace a label with the correct address. For example,
|
||||
the opcodes that get generated for `jsr zero_out_nametables` depend on where in
|
||||
memory the label `zero_out_nametables` will exist. This is what the
|
||||
configuration file memory section specifies.
|
||||
|
||||
### Contra.cfg SEGMENTS Layout
|
||||
This section specifies defines in which order the .o files should appear in the
|
||||
resulting .nes rom file.
|
||||
|
||||
# References
|
||||
* Trax's [Disassembly of _Contra (US)_](https://www.bwass.org/romhack/contra/)
|
||||
* Trax's [documentation on romhacking.net](https://www.romhacking.net/documents/713/)
|
||||
* [Tomorrow Corporation's Retro Game Internal Series on Contra](http://tomorrowcorporation.com/posts/retro-game-internals)
|
||||
* [Overview of NES Rendering by Austin Morlan](https://austinmorlan.com/posts/nes_rendering_overview/)
|
||||
* [Rom Detective article on Contra](http://www.romdetectives.com/Wiki/index.php?title=Contra_(NES))
|
|
@ -0,0 +1,347 @@
|
|||
src\assets\graphic_data\alt_graphic_data_00.bin 37474 1408
|
||||
src\assets\graphic_data\alt_graphic_data_01.bin 38882 2976
|
||||
src\assets\graphic_data\alt_graphic_data_02.bin 41858 928
|
||||
src\assets\graphic_data\alt_graphic_data_03.bin 42786 1088
|
||||
src\assets\graphic_data\alt_graphic_data_04.bin 43874 1184
|
||||
src\assets\graphic_data\graphic_data_01.bin 76349 3724
|
||||
src\assets\graphic_data\graphic_data_02.bin 37031 443
|
||||
src\assets\graphic_data\graphic_data_03.bin 65553 1453
|
||||
src\assets\graphic_data\graphic_data_04.bin 67006 499
|
||||
src\assets\graphic_data\graphic_data_05.bin 81937 2656
|
||||
src\assets\graphic_data\graphic_data_06.bin 72204 1543
|
||||
src\assets\graphic_data\graphic_data_07.bin 84593 2431
|
||||
src\assets\graphic_data\graphic_data_08.bin 67708 4449
|
||||
src\assets\graphic_data\graphic_data_09.bin 72157 47
|
||||
src\assets\graphic_data\graphic_data_0a.bin 73749 833
|
||||
src\assets\graphic_data\graphic_data_0b.bin 87024 3899
|
||||
src\assets\graphic_data\graphic_data_0c.bin 98321 3291
|
||||
src\assets\graphic_data\graphic_data_0d.bin 101612 3834
|
||||
src\assets\graphic_data\graphic_data_0e.bin 105446 5284
|
||||
src\assets\graphic_data\graphic_data_0f.bin 74582 161
|
||||
src\assets\graphic_data\graphic_data_11.bin 74743 1369
|
||||
src\assets\graphic_data\graphic_data_12.bin 76112 237
|
||||
src\assets\graphic_data\graphic_data_13.bin 67505 203
|
||||
src\assets\graphic_data\graphic_data_14.bin 92196 1483
|
||||
src\assets\graphic_data\graphic_data_15.bin 110730 226
|
||||
src\assets\graphic_data\graphic_data_16.bin 110956 262
|
||||
src\assets\graphic_data\graphic_data_17.bin 93679 1326
|
||||
src\assets\graphic_data\graphic_data_18.bin 95005 81
|
||||
src\assets\graphic_data\graphic_data_19.bin 90923 485
|
||||
src\assets\graphic_data\graphic_data_1a.bin 91408 788
|
||||
src\assets\audio_data\sound_02.bin 18962 8
|
||||
src\assets\audio_data\sound_03.bin 18997 17
|
||||
src\assets\audio_data\sound_04.bin 19014 11
|
||||
src\assets\audio_data\sound_05.bin 19025 55
|
||||
src\assets\audio_data\sound_06.bin 19080 10
|
||||
src\assets\audio_data\sound_07.bin 19090 14
|
||||
src\assets\audio_data\sound_08.bin 19104 8
|
||||
src\assets\audio_data\sound_08_part_00.bin 19114 25
|
||||
src\assets\audio_data\sound_09.bin 19601 57
|
||||
src\assets\audio_data\sound_0a.bin 19139 41
|
||||
src\assets\audio_data\sound_0b.bin 19180 16
|
||||
src\assets\audio_data\sound_0b_part_00.bin 19198 23
|
||||
src\assets\audio_data\sound_0c.bin 19221 14
|
||||
src\assets\audio_data\sound_0c_part_00.bin 19237 29
|
||||
src\assets\audio_data\sound_0d.bin 19266 14
|
||||
src\assets\audio_data\sound_0d_part_00.bin 19282 23
|
||||
src\assets\audio_data\sound_0e.bin 19305 55
|
||||
src\assets\audio_data\sound_0f.bin 19360 99
|
||||
src\assets\audio_data\sound_10.bin 19459 12
|
||||
src\assets\audio_data\sound_10_part_00.bin 19473 6
|
||||
src\assets\audio_data\sound_11.bin 19479 10
|
||||
src\assets\audio_data\sound_11_part_00.bin 19491 6
|
||||
src\assets\audio_data\sound_12.bin 19497 14
|
||||
src\assets\audio_data\sound_13.bin 19511 90
|
||||
src\assets\audio_data\sound_14.bin 19658 10
|
||||
src\assets\audio_data\sound_14_part_00.bin 19670 14
|
||||
src\assets\audio_data\sound_15.bin 19801 45
|
||||
src\assets\audio_data\sound_16.bin 19684 35
|
||||
src\assets\audio_data\sound_17.bin 19719 37
|
||||
src\assets\audio_data\sound_18.bin 19756 13
|
||||
src\assets\audio_data\sound_18_part_00.bin 19771 8
|
||||
src\assets\audio_data\sound_19.bin 19779 22
|
||||
src\assets\audio_data\sound_1a.bin 19949 82
|
||||
src\assets\audio_data\sound_1b.bin 19846 5
|
||||
src\assets\audio_data\sound_1b_part_00.bin 19860 42
|
||||
src\assets\audio_data\sound_1b_part_02.bin 19910 39
|
||||
src\assets\audio_data\sound_1c.bin 20149 8
|
||||
src\assets\audio_data\sound_1c_part_00.bin 20159 23
|
||||
src\assets\audio_data\sound_1d.bin 20182 6
|
||||
src\assets\audio_data\sound_1d_part_00.bin 20190 21
|
||||
src\assets\audio_data\sound_1e.bin 20031 24
|
||||
src\assets\audio_data\sound_1f.bin 20055 23
|
||||
src\assets\audio_data\sound_20.bin 20078 71
|
||||
src\assets\audio_data\sound_21.bin 20211 13
|
||||
src\assets\audio_data\sound_21_part_00.bin 20224 11
|
||||
src\assets\audio_data\sound_21_part_02.bin 20250 11
|
||||
src\assets\audio_data\sound_21_part_03.bin 20263 11
|
||||
src\assets\audio_data\sound_21_part_04.bin 20276 11
|
||||
src\assets\audio_data\sound_21_part_05.bin 20289 11
|
||||
src\assets\audio_data\sound_21_part_06.bin 20302 11
|
||||
src\assets\audio_data\sound_21_part_07.bin 20315 11
|
||||
src\assets\audio_data\sound_21_part_08.bin 20328 12
|
||||
src\assets\audio_data\sound_21_part_09.bin 20342 12
|
||||
src\assets\audio_data\sound_21_part_0a.bin 20356 12
|
||||
src\assets\audio_data\sound_23.bin 20371 2
|
||||
src\assets\audio_data\sound_23_part_00.bin 20373 8
|
||||
src\assets\audio_data\sound_23_part_01.bin 20383 6
|
||||
src\assets\audio_data\sound_23_part_02.bin 20391 6
|
||||
src\assets\audio_data\sound_23_part_03.bin 20399 6
|
||||
src\assets\audio_data\sound_23_part_04.bin 20407 6
|
||||
src\assets\audio_data\sound_23_part_05.bin 20415 6
|
||||
src\assets\audio_data\sound_23_part_06.bin 20423 8
|
||||
src\assets\audio_data\sound_24.bin 20434 8
|
||||
src\assets\audio_data\sound_24_part_00.bin 20444 53
|
||||
src\assets\audio_data\sound_25.bin 20497 8
|
||||
src\assets\audio_data\sound_25_part_00.bin 20507 42
|
||||
src\assets\audio_data\sound_25_part_01.bin 20549 7
|
||||
src\assets\audio_data\sound_26.bin 20901 22
|
||||
src\assets\audio_data\sound_27.bin 20923 24
|
||||
src\assets\audio_data\sound_28.bin 20947 16
|
||||
src\assets\audio_data\sound_29.bin 20963 10
|
||||
src\assets\audio_data\sound_2a.bin 21560 29
|
||||
src\assets\audio_data\sound_2a_part_00.bin 21589 21
|
||||
src\assets\audio_data\sound_2a_part_01.bin 21612 361
|
||||
src\assets\audio_data\sound_2b_part_00.bin 21115 26
|
||||
src\assets\audio_data\sound_2b_part_01.bin 21143 415
|
||||
src\assets\audio_data\sound_2c.bin 21975 24
|
||||
src\assets\audio_data\sound_2c_part_00.bin 21999 39
|
||||
src\assets\audio_data\sound_2c_part_01.bin 22040 180
|
||||
src\assets\audio_data\sound_2c_part_02.bin 22220 5
|
||||
src\assets\audio_data\sound_2c_part_03.bin 22227 6
|
||||
src\assets\audio_data\sound_2c_part_04.bin 22235 168
|
||||
src\assets\audio_data\sound_2d.bin 22405 7
|
||||
src\assets\audio_data\sound_2d_part_00.bin 22414 4
|
||||
src\assets\audio_data\sound_2d_part_01.bin 22418 34
|
||||
src\assets\audio_data\sound_2d_part_02.bin 22454 176
|
||||
src\assets\audio_data\sound_2d_part_03.bin 22630 18
|
||||
src\assets\audio_data\sound_2d_part_04.bin 22650 13
|
||||
src\assets\audio_data\sound_2d_part_05.bin 22663 17
|
||||
src\assets\audio_data\sound_2d_part_06.bin 22682 16
|
||||
src\assets\audio_data\sound_2d_part_07.bin 22698 17
|
||||
src\assets\audio_data\sound_2d_part_08.bin 22717 12
|
||||
src\assets\audio_data\sound_2d_part_09.bin 22729 3
|
||||
src\assets\audio_data\sound_2d_part_0a.bin 22734 10
|
||||
src\assets\audio_data\sound_2e.bin 22933 20
|
||||
src\assets\audio_data\sound_2e_part_00.bin 22841 57
|
||||
src\assets\audio_data\sound_2e_part_01.bin 22900 33
|
||||
src\assets\audio_data\sound_2e_part_03.bin 22956 53
|
||||
src\assets\audio_data\sound_2e_part_04.bin 23011 85
|
||||
src\assets\audio_data\sound_2e_part_05.bin 23098 42
|
||||
src\assets\audio_data\sound_2e_part_06.bin 23142 27
|
||||
src\assets\audio_data\sound_2f.bin 23169 4
|
||||
src\assets\audio_data\sound_2f_part_00.bin 23173 17
|
||||
src\assets\audio_data\sound_2f_part_02.bin 23195 4
|
||||
src\assets\audio_data\sound_2f_part_03.bin 23199 26
|
||||
src\assets\audio_data\sound_2f_part_04.bin 23230 92
|
||||
src\assets\audio_data\sound_2f_part_05.bin 23324 32
|
||||
src\assets\audio_data\sound_2f_part_06.bin 23358 57
|
||||
src\assets\audio_data\sound_30.bin 23415 3
|
||||
src\assets\audio_data\sound_30_part_00.bin 23418 3
|
||||
src\assets\audio_data\sound_30_part_02.bin 23426 28
|
||||
src\assets\audio_data\sound_30_part_03.bin 23459 57
|
||||
src\assets\audio_data\sound_31_part_01.bin 23524 3
|
||||
src\assets\audio_data\sound_31_part_02.bin 23529 31
|
||||
src\assets\audio_data\sound_31_part_03.bin 23562 71
|
||||
src\assets\audio_data\sound_32.bin 23732 6
|
||||
src\assets\audio_data\sound_32_part_00.bin 23681 33
|
||||
src\assets\audio_data\sound_32_part_01.bin 23714 9
|
||||
src\assets\audio_data\sound_32_part_02.bin 23725 7
|
||||
src\assets\audio_data\sound_32_part_03.bin 23738 3
|
||||
src\assets\audio_data\sound_32_part_05.bin 23744 17
|
||||
src\assets\audio_data\sound_32_part_06.bin 23763 20
|
||||
src\assets\audio_data\sound_32_part_07.bin 23783 34
|
||||
src\assets\audio_data\sound_32_part_08.bin 23822 3
|
||||
src\assets\audio_data\sound_32_part_09.bin 23827 3
|
||||
src\assets\audio_data\sound_32_part_0a.bin 23832 42
|
||||
src\assets\audio_data\sound_33.bin 23874 5
|
||||
src\assets\audio_data\sound_33_part_00.bin 23879 3
|
||||
src\assets\audio_data\sound_33_part_01.bin 23884 18
|
||||
src\assets\audio_data\sound_33_part_02.bin 23904 21
|
||||
src\assets\audio_data\sound_33_part_03.bin 23925 36
|
||||
src\assets\audio_data\sound_33_part_04.bin 23966 4
|
||||
src\assets\audio_data\sound_33_part_05.bin 23972 6
|
||||
src\assets\audio_data\sound_34.bin 23978 3
|
||||
src\assets\audio_data\sound_34_part_00.bin 23981 3
|
||||
src\assets\audio_data\sound_34_part_01.bin 23986 16
|
||||
src\assets\audio_data\sound_34_part_02.bin 24004 16
|
||||
src\assets\audio_data\sound_34_part_03.bin 24022 30
|
||||
src\assets\audio_data\sound_34_part_04.bin 24022 3
|
||||
src\assets\audio_data\sound_34_part_05.bin 24057 3
|
||||
src\assets\audio_data\sound_34_part_06.bin 24062 4
|
||||
src\assets\audio_data\sound_34_part_07.bin 24068 3
|
||||
src\assets\audio_data\sound_34_part_08.bin 24071 3
|
||||
src\assets\audio_data\sound_34_part_09.bin 24076 4
|
||||
src\assets\audio_data\sound_34_part_0a.bin 24086 11
|
||||
src\assets\audio_data\sound_34_part_0b.bin 24099 11
|
||||
src\assets\audio_data\sound_35_part_00.bin 24111 3
|
||||
src\assets\audio_data\sound_35_part_02.bin 24122 11
|
||||
src\assets\audio_data\sound_35_part_03.bin 24135 10
|
||||
src\assets\audio_data\sound_35_part_04.bin 24145 11
|
||||
src\assets\audio_data\sound_35_part_05.bin 24158 9
|
||||
src\assets\audio_data\sound_35_part_06.bin 24167 11
|
||||
src\assets\audio_data\sound_35_part_07.bin 24180 11
|
||||
src\assets\audio_data\sound_36_part_00.bin 24255 54
|
||||
src\assets\audio_data\sound_36_part_01.bin 24309 17
|
||||
src\assets\audio_data\sound_36_part_02.bin 24328 19
|
||||
src\assets\audio_data\sound_36_part_03.bin 24349 55
|
||||
src\assets\audio_data\sound_37.bin 24406 6
|
||||
src\assets\audio_data\sound_37_part_00.bin 24412 8
|
||||
src\assets\audio_data\sound_37_part_01.bin 24420 20
|
||||
src\assets\audio_data\sound_37_part_02.bin 24442 4
|
||||
src\assets\audio_data\sound_37_part_03.bin 24446 9
|
||||
src\assets\audio_data\sound_37_part_04.bin 24457 11
|
||||
src\assets\audio_data\sound_37_part_05.bin 24470 48
|
||||
src\assets\audio_data\sound_38.bin 24520 4
|
||||
src\assets\audio_data\sound_38_part_00.bin 24524 6
|
||||
src\assets\audio_data\sound_38_part_01.bin 24530 5
|
||||
src\assets\audio_data\sound_38_part_02.bin 24537 3
|
||||
src\assets\audio_data\sound_38_part_03.bin 24540 5
|
||||
src\assets\audio_data\sound_38_part_04.bin 24547 5
|
||||
src\assets\audio_data\sound_38_part_05.bin 24552 7
|
||||
src\assets\audio_data\sound_38_part_06.bin 24561 10
|
||||
src\assets\audio_data\sound_38_part_07.bin 24573 20
|
||||
src\assets\audio_data\sound_39_part_00.bin 24596 3
|
||||
src\assets\audio_data\sound_39_part_01.bin 24601 4
|
||||
src\assets\audio_data\sound_39_part_02.bin 24605 12
|
||||
src\assets\audio_data\sound_39_part_03.bin 24619 10
|
||||
src\assets\audio_data\sound_39_part_04.bin 24629 12
|
||||
src\assets\audio_data\sound_39_part_05.bin 24643 10
|
||||
src\assets\audio_data\sound_39_part_06.bin 24653 12
|
||||
src\assets\audio_data\sound_39_part_07.bin 24667 11
|
||||
src\assets\audio_data\sound_39_part_08.bin 24678 5
|
||||
src\assets\audio_data\sound_39_part_09.bin 24685 5
|
||||
src\assets\audio_data\sound_3a_part_00.bin 24747 33
|
||||
src\assets\audio_data\sound_3a_part_01.bin 24782 7
|
||||
src\assets\audio_data\sound_3a_part_02.bin 24789 15
|
||||
src\assets\audio_data\sound_3a_part_03.bin 24806 16
|
||||
src\assets\audio_data\sound_3a_part_04.bin 24822 6
|
||||
src\assets\audio_data\sound_3a_part_05.bin 24830 38
|
||||
src\assets\audio_data\sound_3a_part_06.bin 24868 6
|
||||
src\assets\audio_data\sound_3a_part_07.bin 24876 39
|
||||
src\assets\audio_data\sound_3a_part_08.bin 24915 6
|
||||
src\assets\audio_data\sound_3a_part_09.bin 24923 15
|
||||
src\assets\audio_data\sound_3a_part_0a.bin 24938 6
|
||||
src\assets\audio_data\sound_3a_part_0b.bin 24946 67
|
||||
src\assets\audio_data\sound_3b.bin 25015 12
|
||||
src\assets\audio_data\sound_3b_part_00.bin 25027 30
|
||||
src\assets\audio_data\sound_3b_part_01.bin 25059 19
|
||||
src\assets\audio_data\sound_3b_part_02.bin 25080 171
|
||||
src\assets\audio_data\sound_3c.bin 25253 11
|
||||
src\assets\audio_data\sound_3c_part_00.bin 25266 8
|
||||
src\assets\audio_data\sound_3c_part_01.bin 25276 26
|
||||
src\assets\audio_data\sound_3c_part_02.bin 25302 20
|
||||
src\assets\audio_data\sound_3c_part_03.bin 25324 46
|
||||
src\assets\audio_data\sound_3c_part_04.bin 25370 6
|
||||
src\assets\audio_data\sound_3c_part_05.bin 25378 27
|
||||
src\assets\audio_data\sound_3d_part_00.bin 25408 3
|
||||
src\assets\audio_data\sound_3d_part_02.bin 25413 8
|
||||
src\assets\audio_data\sound_3d_part_03.bin 25421 24
|
||||
src\assets\audio_data\sound_3d_part_04.bin 25447 12
|
||||
src\assets\audio_data\sound_3d_part_05.bin 25461 10
|
||||
src\assets\audio_data\sound_3d_part_06.bin 25471 14
|
||||
src\assets\audio_data\sound_3d_part_07.bin 25487 90
|
||||
src\assets\audio_data\sound_3e.bin 25720 20
|
||||
src\assets\audio_data\sound_3e_part_00.bin 25644 18
|
||||
src\assets\audio_data\sound_3e_part_01.bin 25664 17
|
||||
src\assets\audio_data\sound_3e_part_02.bin 25681 20
|
||||
src\assets\audio_data\sound_3e_part_03.bin 25703 17
|
||||
src\assets\audio_data\sound_3e_part_04.bin 25742 17
|
||||
src\assets\audio_data\sound_3e_part_05.bin 25759 20
|
||||
src\assets\audio_data\sound_3e_part_06.bin 25781 16
|
||||
src\assets\audio_data\sound_3e_part_07.bin 25797 68
|
||||
src\assets\audio_data\sound_3e_part_08.bin 25867 11
|
||||
src\assets\audio_data\sound_3e_part_09.bin 25880 10
|
||||
src\assets\audio_data\sound_3e_part_0a.bin 25899 21
|
||||
src\assets\audio_data\sound_3e_part_0b.bin 25922 19
|
||||
src\assets\audio_data\sound_3e_part_0c.bin 25941 21
|
||||
src\assets\audio_data\sound_3e_part_0d.bin 25964 20
|
||||
src\assets\audio_data\sound_3f.bin 25984 3
|
||||
src\assets\audio_data\sound_3f_part_00.bin 25989 66
|
||||
src\assets\audio_data\sound_3f_part_01.bin 26057 14
|
||||
src\assets\audio_data\sound_3f_part_02.bin 26073 14
|
||||
src\assets\audio_data\sound_3f_part_03.bin 26093 3
|
||||
src\assets\audio_data\sound_3f_part_04.bin 26096 3
|
||||
src\assets\audio_data\sound_3f_part_05.bin 26101 3
|
||||
src\assets\audio_data\sound_40.bin 26107 10
|
||||
src\assets\audio_data\sound_40_part_00.bin 26119 4
|
||||
src\assets\audio_data\sound_40_part_01.bin 26125 49
|
||||
src\assets\audio_data\sound_40_part_02.bin 26176 9
|
||||
src\assets\audio_data\sound_40_part_03.bin 26187 10
|
||||
src\assets\audio_data\sound_40_part_04.bin 26206 10
|
||||
src\assets\audio_data\sound_40_part_05.bin 26218 9
|
||||
src\assets\audio_data\sound_40_part_06.bin 26227 10
|
||||
src\assets\audio_data\sound_40_part_07.bin 26239 11
|
||||
src\assets\audio_data\sound_41.bin 26250 8
|
||||
src\assets\audio_data\sound_41_part_00.bin 26260 7
|
||||
src\assets\audio_data\sound_41_part_01.bin 26267 10
|
||||
src\assets\audio_data\sound_41_part_02.bin 26279 9
|
||||
src\assets\audio_data\sound_41_part_03.bin 26288 14
|
||||
src\assets\audio_data\sound_41_part_04.bin 26304 16
|
||||
src\assets\audio_data\sound_41_part_05.bin 26322 34
|
||||
src\assets\audio_data\sound_41_part_06.bin 26356 6
|
||||
src\assets\audio_data\sound_41_part_07.bin 26364 4
|
||||
src\assets\audio_data\sound_41_part_08.bin 26368 6
|
||||
src\assets\audio_data\sound_41_part_09.bin 26376 8
|
||||
src\assets\audio_data\sound_42.bin 26531 91
|
||||
src\assets\audio_data\sound_42_part_00.bin 26624 134
|
||||
src\assets\audio_data\sound_43.bin 26760 5
|
||||
src\assets\audio_data\sound_43_part_00.bin 26437 15
|
||||
src\assets\audio_data\sound_43_part_01.bin 26454 17
|
||||
src\assets\audio_data\sound_43_part_02.bin 26473 19
|
||||
src\assets\audio_data\sound_43_part_03.bin 26494 37
|
||||
src\assets\audio_data\sound_43_part_04.bin 26770 119
|
||||
src\assets\audio_data\sound_44.bin 26891 135
|
||||
src\assets\audio_data\sound_44_part_00.bin 27028 119
|
||||
src\assets\audio_data\sound_45.bin 27166 27
|
||||
src\assets\audio_data\sound_45_part_00.bin 27149 6
|
||||
src\assets\audio_data\sound_45_part_01.bin 27155 8
|
||||
src\assets\audio_data\sound_45_part_02.bin 27195 25
|
||||
src\assets\audio_data\sound_45_part_03.bin 27231 14
|
||||
src\assets\audio_data\sound_46.bin 27298 33
|
||||
src\assets\audio_data\sound_47.bin 27331 33
|
||||
src\assets\audio_data\sound_48.bin 27364 27
|
||||
src\assets\audio_data\sound_49.bin 27391 21
|
||||
src\assets\audio_data\sound_4a.bin 27823 2
|
||||
src\assets\audio_data\sound_4a_part_00.bin 27644 179
|
||||
src\assets\audio_data\sound_4a_part_01.bin 27827 4
|
||||
src\assets\audio_data\sound_4a_part_02.bin 27833 4
|
||||
src\assets\audio_data\sound_4a_part_03.bin 27837 109
|
||||
src\assets\audio_data\sound_4b_part_00.bin 27949 3
|
||||
src\assets\audio_data\sound_4b_part_01.bin 27954 3
|
||||
src\assets\audio_data\sound_4b_part_02.bin 27957 15
|
||||
src\assets\audio_data\sound_4b_part_03.bin 27974 35
|
||||
src\assets\audio_data\sound_4b_part_04.bin 28009 14
|
||||
src\assets\audio_data\sound_4b_part_05.bin 28025 15
|
||||
src\assets\audio_data\sound_4b_part_06.bin 28042 35
|
||||
src\assets\audio_data\sound_4b_part_07.bin 28077 14
|
||||
src\assets\audio_data\sound_4b_part_08.bin 28093 88
|
||||
src\assets\audio_data\sound_4c_part_00.bin 28184 14
|
||||
src\assets\audio_data\sound_4c_part_01.bin 28200 6
|
||||
src\assets\audio_data\sound_4c_part_02.bin 28206 12
|
||||
src\assets\audio_data\sound_4c_part_03.bin 28220 10
|
||||
src\assets\audio_data\sound_4c_part_04.bin 28230 11
|
||||
src\assets\audio_data\sound_4c_part_05.bin 28243 9
|
||||
src\assets\audio_data\sound_4c_part_06.bin 28252 11
|
||||
src\assets\audio_data\sound_4c_part_07.bin 28265 10
|
||||
src\assets\audio_data\sound_4c_part_08.bin 28275 11
|
||||
src\assets\audio_data\sound_4c_part_09.bin 28288 9
|
||||
src\assets\audio_data\sound_4c_part_0a.bin 28297 11
|
||||
src\assets\audio_data\sound_4d_part_00.bin 28314 11
|
||||
src\assets\audio_data\sound_4d_part_01.bin 28327 6
|
||||
src\assets\audio_data\sound_4e.bin 27460 40
|
||||
src\assets\audio_data\sound_4f.bin 27500 42
|
||||
src\assets\audio_data\sound_50.bin 27542 44
|
||||
src\assets\audio_data\sound_51.bin 27586 32
|
||||
src\assets\audio_data\sound_52.bin 20775 83
|
||||
src\assets\audio_data\sound_53.bin 20858 43
|
||||
src\assets\audio_data\sound_54.bin 18970 26
|
||||
src\assets\audio_data\sound_55.bin 20556 25
|
||||
src\assets\audio_data\sound_56.bin 20581 8
|
||||
src\assets\audio_data\sound_56_part_00.bin 20591 53
|
||||
src\assets\audio_data\sound_57.bin 20644 4
|
||||
src\assets\audio_data\sound_58.bin 20648 68
|
||||
src\assets\audio_data\sound_59.bin 20716 8
|
||||
src\assets\audio_data\sound_59_part_00.bin 20726 49
|
||||
src\assets\audio_data\dpcm_sample_00.bin 130064 81
|
||||
src\assets\audio_data\dpcm_sample_01.bin 130256 593
|
|
@ -0,0 +1,54 @@
|
|||
@echo off
|
||||
|
||||
rem Assembles and links the source assemblies into a .nes ROM.
|
||||
rem Run this script from the windows command prompt.
|
||||
rem If desired, there is also a build.ps1 powershell script
|
||||
rem that can be used as well.
|
||||
|
||||
IF EXIST contra.nes (
|
||||
echo Deleting contra.nes.
|
||||
del contra.nes
|
||||
)
|
||||
|
||||
IF NOT EXIST "obj" (
|
||||
mkdir "obj"
|
||||
)
|
||||
|
||||
IF EXIST "obj\*.o" (
|
||||
echo Deleting object files.
|
||||
del "obj\*.o"
|
||||
)
|
||||
|
||||
IF NOT EXIST base.nes (
|
||||
echo No baserom.nes file found. If assets are missing, then the build will fail.
|
||||
)
|
||||
|
||||
rem show commands run in output
|
||||
echo Assembling PRG Rom Banks
|
||||
|
||||
rem loop through assets defined in assets.txt and extract bytes from baserom.nes
|
||||
echo Extracting binary data from baserom.nes
|
||||
for /f "tokens=1,2,3 delims= " %%i in (assets.txt) do (
|
||||
cscript /nologo set_bytes.vbs %%j %%k %%i
|
||||
)
|
||||
|
||||
@echo on
|
||||
ca65 -g --debug-info -o obj\constants.o src\constants.asm
|
||||
ca65 -g --debug-info -o obj\ines_header.o src\ines_header.asm
|
||||
ca65 -g --debug-info -o obj\bank0.o src\bank0.asm
|
||||
ca65 -g --debug-info -o obj\bank1.o src\bank1.asm
|
||||
ca65 -g --debug-info -o obj\bank2.o src\bank2.asm
|
||||
ca65 -g --debug-info -o obj\bank3.o src\bank3.asm
|
||||
ca65 -g --debug-info -o obj\bank4.o src\bank4.asm
|
||||
ca65 -g --debug-info -o obj\bank5.o src\bank5.asm
|
||||
ca65 -g --debug-info -o obj\bank6.o src\bank6.asm
|
||||
ca65 -g --debug-info -o obj\bank7.o src\bank7.asm
|
||||
@echo off
|
||||
|
||||
rem link assemblies together to single .nes ROM
|
||||
|
||||
echo "Creating .nes ROM"
|
||||
|
||||
@echo on
|
||||
ld65 -C contra.cfg --dbgfile contra.dbg .\obj\constants.o .\obj\ines_header.o .\obj\bank0.o .\obj\bank1.o .\obj\bank2.o .\obj\bank3.o .\obj\bank4.o .\obj\bank5.o .\obj\bank6.o .\obj\bank7.o -o contra.nes
|
||||
@echo off
|
|
@ -0,0 +1,97 @@
|
|||
# Assembles and links the source assemblies into a .nes ROM.
|
||||
# Run this script from powershell if available, if not available use build.bat
|
||||
# batch file from the windows command prompt.
|
||||
|
||||
$global:SOURCE_CONTRA = $null
|
||||
$US_CONTRA_HASH = "1C747C78C678F14A68D4E5FCAE065298A103F833638775860F0E5C5FFAA061F62D45FD8942148B1507C1FD57FDE950A5D83F9F84A9782EC048A56067740C48E9"
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
|
||||
Copies bytes from the Contra US NES rom file into binary files for use when
|
||||
assembling.
|
||||
#>
|
||||
function Set-Bytes {
|
||||
param ($Skip, $Take, $Output)
|
||||
|
||||
IF (Test-Path -Path $Output) {
|
||||
return
|
||||
}
|
||||
|
||||
# only read baserom.nes once for speed improvements
|
||||
IF ($global:SOURCE_CONTRA -eq $null) {
|
||||
Write-Output " Reading input file baserom.nes."
|
||||
$global:SOURCE_CONTRA = Get-Content .\baserom.nes -AsByteStream
|
||||
}
|
||||
|
||||
Write-Output " Writing file $Output."
|
||||
$global:SOURCE_CONTRA | Select-Object -Skip $Skip -First $Take | Set-Content $Output -AsByteStream
|
||||
}
|
||||
|
||||
IF (Test-Path -Path "contra.nes") {
|
||||
Write-Output "Deleting contra.nes."
|
||||
Remove-Item -Path "contra.nes"
|
||||
}
|
||||
|
||||
IF (-not (Test-Path -Path "obj")) {
|
||||
New-Item -ItemType Directory -Path obj
|
||||
}
|
||||
|
||||
IF (Test-Path -Path obj\*.o) {
|
||||
Write-Output "Deleting object files."
|
||||
Remove-Item -Path obj\*.o
|
||||
}
|
||||
|
||||
IF (-not (Test-Path -Path "baserom.nes")) {
|
||||
Write-Output "No baserom.nes file found. If assets are missing, then the build will fail."
|
||||
} ELSE {
|
||||
$SHA512_HASH = (Get-FileHash baserom.nes -Algorithm SHA512).Hash
|
||||
IF ($SHA512_HASH -ne $US_CONTRA_HASH) {
|
||||
Write-Warning "baserom.nes file integrity does NOT match expected result."
|
||||
}
|
||||
}
|
||||
|
||||
# loop through assets defined in assets.txt and extract bytes from baserom.nes
|
||||
Write-Output "Extracting binary data from baserom.nes"
|
||||
ForEach ($line in Get-Content -Path assets.txt) {
|
||||
$tokens = -split $line
|
||||
Set-Bytes -Skip $tokens[1] -Take $tokens[2] -Output $tokens[0]
|
||||
}
|
||||
|
||||
# prevent write race condition
|
||||
Start-Sleep -Milliseconds 100
|
||||
|
||||
Write-Output "Assembling PRG Rom Banks"
|
||||
|
||||
# show commands run in output
|
||||
Set-PSDebug -Trace 1
|
||||
ca65 -g --debug-info -o obj\constants.o src\constants.asm
|
||||
ca65 -g --debug-info -o obj\ines_header.o src\ines_header.asm
|
||||
ca65 -g --debug-info -o obj\bank0.o src\bank0.asm
|
||||
ca65 -g --debug-info -o obj\bank1.o src\bank1.asm
|
||||
ca65 -g --debug-info -o obj\bank2.o src\bank2.asm
|
||||
ca65 -g --debug-info -o obj\bank3.o src\bank3.asm
|
||||
ca65 -g --debug-info -o obj\bank4.o src\bank4.asm
|
||||
ca65 -g --debug-info -o obj\bank5.o src\bank5.asm
|
||||
ca65 -g --debug-info -o obj\bank6.o src\bank6.asm
|
||||
ca65 -g --debug-info -o obj\bank7.o src\bank7.asm
|
||||
|
||||
Set-PSDebug -Trace 0
|
||||
|
||||
# link assemblies together to single .nes ROM
|
||||
Write-Output "Creating .nes ROM"
|
||||
|
||||
Set-PSDebug -Trace 1
|
||||
ld65 -C contra.cfg --dbgfile contra.dbg .\obj\constants.o .\obj\ines_header.o .\obj\bank0.o .\obj\bank1.o .\obj\bank2.o .\obj\bank3.o .\obj\bank4.o .\obj\bank5.o .\obj\bank6.o .\obj\bank7.o -o contra.nes
|
||||
|
||||
# compare assembled ROM hash to expected hash if file exists
|
||||
Set-PSDebug -Trace 0
|
||||
IF (Test-Path -Path "contra.nes") {
|
||||
$SHA512_HASH = (Get-FileHash contra.nes -Algorithm SHA512).Hash
|
||||
|
||||
IF ($SHA512_HASH -eq $US_CONTRA_HASH) {
|
||||
Write-Output "File integrity matches."
|
||||
} ELSE {
|
||||
Write-Warning "File integrity does NOT match."
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Assembles and links the source assemblies into a .nes ROM.
|
||||
|
||||
# Run this script from a bash terminal if on linux or mac.
|
||||
# If you are on windows, use either build.ps1, or build.bat
|
||||
|
||||
us_contra_hash=1c747c78c678f14a68d4e5fcae065298a103f833638775860f0e5c5ffaa061f62d45fd8942148b1507c1fd57fde950a5d83f9f84a9782ec048a56067740c48e9
|
||||
|
||||
setBytes(){
|
||||
if test -f $3
|
||||
then
|
||||
return
|
||||
fi
|
||||
|
||||
echo " Writing file $3."
|
||||
dd bs=1 skip=$1 count=$2 if=baserom.nes of=$3 status=none
|
||||
}
|
||||
|
||||
if ! ld65 --version &> /dev/null
|
||||
then
|
||||
echo "cc65 compiler suite could not be found. Please install cc65 and add it to your path."
|
||||
exit
|
||||
fi
|
||||
|
||||
mkdir -p obj
|
||||
|
||||
if test -f "contra.nes"
|
||||
then
|
||||
echo "Deleting contra.nes."
|
||||
rm contra.nes
|
||||
fi
|
||||
|
||||
if test -f "obj/*.o"
|
||||
then
|
||||
echo "Deleting object files."
|
||||
rm obj/*.o
|
||||
fi
|
||||
|
||||
if ! test -f "baserom.nes"
|
||||
then
|
||||
echo "No baserom.nes file found. If assets are missing, then the build will fail."
|
||||
else
|
||||
ROM_HASH=$(sha512sum baserom.nes | awk '{print $1}')
|
||||
if [[ "$ROM_HASH" != $us_contra_hash ]]
|
||||
then
|
||||
echo "baserom.nes file integrity does NOT match expected result."
|
||||
fi
|
||||
fi
|
||||
|
||||
# loop through assets defined in assets.txt and extract bytes from baserom.nes
|
||||
echo "Extracting binary data from baserom.nes"
|
||||
while read -r line || [ -n "$p" ]
|
||||
do
|
||||
set $line
|
||||
file=$1
|
||||
start=$2
|
||||
length=$3
|
||||
length=$(echo $length | tr -d '\r')
|
||||
setBytes $start $length $file
|
||||
done < assets.txt
|
||||
|
||||
echo "Assembling PRG Rom Banks"
|
||||
ca65 -g --debug-info -o obj/constants.o src/constants.asm
|
||||
ca65 -g --debug-info -o obj/ines_header.o src/ines_header.asm
|
||||
ca65 -g --debug-info -o obj/bank0.o src/bank0.asm
|
||||
ca65 -g --debug-info -o obj/bank1.o src/bank1.asm
|
||||
ca65 -g --debug-info -o obj/bank2.o src/bank2.asm
|
||||
ca65 -g --debug-info -o obj/bank3.o src/bank3.asm
|
||||
ca65 -g --debug-info -o obj/bank4.o src/bank4.asm
|
||||
ca65 -g --debug-info -o obj/bank5.o src/bank5.asm
|
||||
ca65 -g --debug-info -o obj/bank6.o src/bank6.asm
|
||||
ca65 -g --debug-info -o obj/bank7.o src/bank7.asm
|
||||
|
||||
echo "Creating .nes ROM"
|
||||
ld65 -C contra.cfg --dbgfile contra.dbg ./obj/constants.o ./obj/ines_header.o ./obj/bank0.o ./obj/bank1.o ./obj/bank2.o ./obj/bank3.o ./obj/bank4.o ./obj/bank5.o ./obj/bank6.o ./obj/bank7.o -o contra.nes
|
||||
|
||||
if test -f "contra.nes"
|
||||
then
|
||||
# compare assembled ROM hash to expected hash
|
||||
ROM_HASH=$(sha512sum contra.nes | awk '{print $1}')
|
||||
if [[ "$ROM_HASH" == $us_contra_hash ]]
|
||||
then
|
||||
echo "File integrity matches."
|
||||
else
|
||||
echo "File integrity does NOT match."
|
||||
fi
|
||||
fi
|
|
@ -0,0 +1,37 @@
|
|||
# defines where the banks will be loaded into CPU address space
|
||||
MEMORY {
|
||||
# INES Cartridge Header, not loaded into CPU memory
|
||||
HEADER: start = $0000, size = $0010, fill = yes;
|
||||
|
||||
OAMRAM: file="", start = $0200, size = $0100, type = rw;
|
||||
|
||||
# UxROM (002) bank layout
|
||||
BANK_0: start = $8000, size = $4000, type = ro, fill = yes, fillval = $FF;
|
||||
BANK_1: start = $8000, size = $4000, type = ro, fill = yes, fillval = $FF;
|
||||
BANK_2: start = $8000, size = $4000, type = ro, fill = yes, fillval = $FF;
|
||||
BANK_3: start = $8000, size = $4000, type = ro, fill = yes, fillval = $FF;
|
||||
BANK_4: start = $8000, size = $4000, type = ro, fill = yes, fillval = $FF;
|
||||
BANK_5: start = $8000, size = $4000, type = ro, fill = yes, fillval = $FF;
|
||||
BANK_6: start = $8000, size = $4000, type = ro, fill = yes, fillval = $FF;
|
||||
BANK_7: start = $C000, size = $3FFA, type = ro, fill = yes, fillval = $FF;
|
||||
# BANK_7 is actually still $4000 bytes large, but the hardware vectors (ROMV),
|
||||
# which are on BANK7, are counted separately
|
||||
|
||||
# Hardware Vectors at End of 2nd 8K ROM (NES hard-coded location)
|
||||
ROMV: start = $FFFA, size = $0006, type = ro, fill = yes;
|
||||
}
|
||||
|
||||
# defines the order of the segments as they are stored in the .nes ROM file
|
||||
SEGMENTS {
|
||||
HEADER: load = HEADER, type = ro;
|
||||
OAMRAM: load = OAMRAM, type = bss, define=yes, optional=yes;
|
||||
BANK_0: load = BANK_0, type = ro;
|
||||
BANK_1: load = BANK_1, type = ro;
|
||||
BANK_2: load = BANK_2, type = ro;
|
||||
BANK_3: load = BANK_3, type = ro;
|
||||
BANK_4: load = BANK_4, type = ro;
|
||||
BANK_5: load = BANK_5, type = ro;
|
||||
BANK_6: load = BANK_6, type = ro;
|
||||
BANK_7: load = BANK_7, type = ro;
|
||||
VECTORS: load = ROMV, type = ro;
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
# Aiming
|
||||
|
||||
16 enemies aim towards the player to target them. Some of those enemies, for
|
||||
example a sniper (enemy type = #$06), will aim towards the closest player and
|
||||
fire bullets. Others will track the player actively. For example, the white
|
||||
blobs (enemy type = #$13) will follow the player for a specified amount of time.
|
||||
Note that some enemies will fire, but they aren't aiming like the scuba soldier,
|
||||
or even the regular soldier.
|
||||
|
||||
Some enemies can aim in more directions than others. For example, snipers
|
||||
(enemy type = #$06) can aim in #$17 directions, whereas rotating guns (enemy
|
||||
type = #$04) can only aim in #$0b directions. The number of directions that an
|
||||
enemy can aim are determined by which table is used when aiming.
|
||||
|
||||
* `quadrant_aim_dir_00` is used for outdoor enemies and has #$03 aiming
|
||||
directions per quadrant for a total of #$0b aiming directions.
|
||||
* `quadrant_aim_dir_01` is used for all indoor/base enemies, tank, sniper, white
|
||||
blob and spinning bubbles. The table has values #$06 aiming directions per
|
||||
quadrant for a total of #$17 aiming directions.
|
||||
* `quadrant_aim_dir_02` is only used by dragon arm when seeking (stage 3 boss)
|
||||
and has #$0f aiming directions per quadrant for a total of #$3b aiming
|
||||
directions.
|
||||
|
||||
Aiming direction values start at #$00, which represents 3 o'clock. Incrementing
|
||||
the aiming directions moves the direction clockwise.
|
||||
|
||||
The main method to determine what value within a quadrant to aim is
|
||||
`get_quadrant_aim_dir`. This method targets the location ($0b, $0a) from
|
||||
position ($09, $08). This location is almost always the location of the closest
|
||||
player to the enemy on the x-axis, which is determined by the method
|
||||
`player_enemy_x_dist`. However, white blobs (enemy type = #$13) will target
|
||||
players randomly based on the frame counter.
|
||||
|
||||
`get_quadrant_aim_dir` determines where the player is in relation to the enemy,
|
||||
i.e. above or below, to the left or to the right. It will also calculate an
|
||||
index into a single quadrant that most closely targets the player position based
|
||||
on `quadrant_aim_dir_xx`. The quadrant aim direction is then converted to a
|
||||
full aim direction according to the table below. Essentially, the quadrant code
|
||||
is either added or subtracted from the x-axis aim direction depending on the
|
||||
relative quadrant of the player in relation to the enemy.
|
||||
|
||||
| Quadrant | Math | Description |
|
||||
|----------|-------------------------------------------------------|------------------------------------------------|
|
||||
| I | new_aim_dir = max_aim_dir - new_aim_dir | result subtracted from 3 o'cock aim direction |
|
||||
| II | new_aim_dir = max_aim_dir - mid_aim_dir + new_aim_dir | result added to 9 o'clock aim direction |
|
||||
| III | new_aim_dir = mid_aim_dir - new_aim_dir | result subtracted from 9 o'clock aim direction |
|
||||
| IV | new_aim_dir | no math |
|
||||
|
||||
Many enemies will use the full aim direction value to set their graphic to the
|
||||
appropriate value. For example, the rotating gun will take the calculated full
|
||||
aim direction from `aim_var_1_for_quadrant_aim_dir_00`, adjust it by adding or
|
||||
subtracting, then setting the super-tile to the value as an offset into
|
||||
`level_xx_nametable_update_supertile_data`.
|
||||
|
||||
## Bullets
|
||||
|
||||
When creating bullets, the value returned by `get_quadrant_aim_dir` is converted
|
||||
into the full aim direction. Then the full aim direction is used by
|
||||
`calc_bullet_velocities` as an offset into `bullet_fract_vel_dir_lookup_tbl`.
|
||||
This value is then used as an offset into `bullet_fract_vel_tbl`. These values,
|
||||
along with the bullet speed code and quadrant, are used to determine the bullet
|
||||
x and y velocities.
|
||||
|
||||
For most enemy generated bullets, the method `aim_and_create_enemy_bullet` is
|
||||
used. This method calls `get_quadrant_aim_dir` and `calc_bullet_velocities`.
|
||||
However, some enemies instead call `get_quadrant_aim_dir_for_player` directly
|
||||
and then call `calc_bullet_velocities` separately to set the bullet velocities.
|
||||
|
||||
| Enemy | Type | Calling Method |
|
||||
|-------------------------------|------|-----------------------------------|
|
||||
| Wall Turret | #$13 | `aim_and_create_enemy_bullet` |
|
||||
| Core | #$14 | `aim_and_create_enemy_bullet` |
|
||||
| Dragon Tentacle Orb | #$15 | `aim_and_create_enemy_bullet` |
|
||||
| Jumping Soldier | #$16 | `aim_and_create_enemy_bullet` |
|
||||
| Boss Eye Fire Ring Projectile | #$1b | `get_quadrant_aim_dir_for_player` |
|
||||
| Gardegura | #$1d | `get_quadrant_aim_dir_for_player` |
|
||||
| Rangel | #$1f | `aim_and_create_enemy_bullet` |
|
||||
|
||||
All of the enemies using this pattern to create bullets use
|
||||
`quadrant_aim_dir_01`, which gives quadrant offsets between 0-6
|
||||
inclusively. Since each quadrant has 6 values, there are 24 (#$18) possible
|
||||
aim directions. Below is the table that is used to to determine the velocity
|
||||
from the calculated full aim direction. Once the velocity is obtained from this
|
||||
table, the quadrant is incorporated to adjust the signs (+/-) on the direction.
|
||||
Finally, the velocity is adjusted again based on bullet speed code
|
||||
(`adjust_bullet_velocity`).
|
||||
|
||||
| `bullet_fract_vel_dir_lookup_tbl` | `bullet_fract_vel_tbl` | x vel | y vel | degree off axis |
|
||||
|-----------------------------------|------------------------|--------------|--------------|-----------------|
|
||||
| #$00 | #$00 | #$ff (.9961) | #$00 | 0° |
|
||||
| #$01 | #$02 | #$f7 (.9648) | #$42 (.2578) | 15° |
|
||||
| #$02 | #$04 | #$dd (.8633) | #$80 (.5) | 30° |
|
||||
| #$03 | #$06 | #$b5 (.7070) | #$b5 (.7070) | 45° |
|
||||
| #$04 | #$08 | #$80 (.5) | #$dd (.8633) | 60° |
|
||||
| #$05 | #$0a | #$42 (.2578) | #$f7 (.9648) | 75° |
|
||||
| #$06 | #$0c | #$00 | #$ff (.9961) | 90° |
|
||||
| #$07 | #$0a | #$42 (.2578) | #$f7 (.9648) | 75° |
|
||||
| #$08 | #$08 | #$80 (.5) | #$dd (.8633) | 60° |
|
||||
| #$09 | #$06 | #$b5 (.7070) | #$b5 (.7070) | 45° |
|
||||
| #$0a | #$04 | #$dd (.8633) | #$80 (.5) | 30° |
|
||||
| #$0b | #$02 | #$f7 (.9648) | #$42 (.2578) | 15° |
|
||||
| #$0c | #$00 | #$00 | #$ff (.9961) | 90° |
|
||||
| #$0d | #$02 | #$f7 (.9648) | #$42 (.2578) | 15° |
|
||||
| #$0e | #$04 | #$dd (.8633) | #$80 (.5) | 30° |
|
||||
| #$0f | #$06 | #$b5 (.7070) | #$b5 (.7070) | 45° |
|
||||
| #$10 | #$08 | #$80 (.5) | #$dd (.8633) | 60° |
|
||||
| #$11 | #$0a | #$42 (.2578) | #$f7 (.9648) | 75° |
|
||||
| #$12 | #$0c | #$00 | #$ff (.9961) | 90° |
|
||||
| #$13 | #$0a | #$42 (.2578) | #$f7 (.9648) | 75° |
|
||||
| #$14 | #$08 | #$80 (.5) | #$dd (.8633) | 60° |
|
||||
| #$15 | #$06 | #$b5 (.7070) | #$b5 (.7070) | 45° |
|
||||
| #$16 | #$04 | #$dd (.8633) | #$80 (.5) | 30° |
|
||||
| #$17 | #$02 | #$f7 (.9648) | #$42 (.2578) | 15° |
|
||||
|
||||
## Dragon seeking
|
||||
|
||||
Dragon Tentacle Orb (enemy type = #$15) is the only enemy type that uses
|
||||
`quadrant_aim_dir_02`. It uses this during the 'arm seeking player' attack
|
||||
pattern (attack pattern #$04). `quadrant_aim_dir_02` has the most precision
|
||||
with #$0f aim directions per quadrant.
|
||||
|
||||
## Aim Call Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
get_rotate_dir_for_index --> get_quadrant_aim_dir
|
||||
get_rotate_00 --> get_rotate_dir_for_index
|
||||
get_rotate_01 --> get_rotate_dir_for_index
|
||||
rotating_gun_routine_03:::enemy --> aim_var_1_for_quadrant_aim_dir_00
|
||||
alien_fetus_routine_01:::enemy --> aim_var_1_for_quadrant_aim_dir_00
|
||||
red_turret_routine_03:::enemy --> get_rotate_00
|
||||
white_blob_routine_00:::enemy --> get_rotate_01
|
||||
sniper_routine_02:::enemy --> get_rotate_01
|
||||
aim_var_1_for_quadrant_aim_dir_00 --> get_rotate_00
|
||||
aim_var_1_for_quadrant_aim_dir_01 --> get_rotate_01
|
||||
spinning_bubbles_routine_01:::enemy --> aim_var_1_for_quadrant_aim_dir_01
|
||||
tank_routine_02:::enemy --> aim_var_1_for_quadrant_aim_dir_01
|
||||
white_blob_aim_to_player:::enemy --> aim_var_1_for_quadrant_aim_dir_01
|
||||
spinning_bubbles_routine_00:::enemy --> get_quadrant_aim_dir_for_player
|
||||
eye_projectile_routine_00:::enemy --> get_quadrant_aim_dir_for_player
|
||||
dragon_arm_orb_seek_should_move:::enemy --> get_quadrant_aim_dir_for_player
|
||||
dragon_arm_orb_fire_projectile:::enemy --> aim_and_create_enemy_bullet
|
||||
aim_and_create_enemy_bullet --> get_quadrant_aim_dir_for_player
|
||||
aim_and_create_enemy_bullet --> get_quadrant_aim_dir
|
||||
get_quadrant_aim_dir_for_player --> get_quadrant_aim_dir
|
||||
wall_turret_routine_03:::enemy --> aim_and_create_enemy_bullet
|
||||
wall_core_routine_03:::enemy --> aim_and_create_enemy_bullet
|
||||
jumping_soldier_routine_01:::enemy --> aim_and_create_enemy_bullet
|
||||
red_soldier_routine_02:::enemy --> aim_and_create_enemy_bullet
|
||||
get_rotate_dir_for_index --> get_quadrant_aim_dir_for_player
|
||||
|
||||
classDef enemy fill:#f96
|
||||
```
|
||||
|
||||
## Pseudocode
|
||||
|
||||
Below is a c-like pseudo-code showing how the quadrant aim direction from
|
||||
`get_quadrant_aim_dir` value is converted to a full aim direction in
|
||||
`get_rotate_dir`.
|
||||
|
||||
```
|
||||
int quadrant_aim_dir = get_quadrant_aim_dir();
|
||||
|
||||
// mid_aim_dir is 9 o'clock
|
||||
// max_aim_dir is 3 o'clock
|
||||
int mid_aim_dir, max_aim_dir, new_aim_dir;
|
||||
if (quadrant_aim_dir_00 || quadrant_aim_dir_02) {
|
||||
mid_aim_dir = 0x06;
|
||||
max_aim_dir = 0x0c;
|
||||
} else {
|
||||
mid_aim_dir = 0x0c;
|
||||
max_aim_dir = 0x18;
|
||||
}
|
||||
|
||||
if(player_left_of_enemy) {
|
||||
new_aim_dir = mid_aim_dir - quadrant_aim_dir;
|
||||
}
|
||||
|
||||
if(player_above_enemy) {
|
||||
if(quadrant_aim_dir != 0x00) {
|
||||
new_aim_dir = max_aim_dir - quadrant_aim_dir;
|
||||
} else {
|
||||
new_aim_dir = 0x00;
|
||||
}
|
||||
}
|
||||
|
||||
// new full aim direction calculated
|
||||
new_aim_dir = quadrant_aim_dir;
|
||||
```
|
|
@ -0,0 +1,68 @@
|
|||
# Overview
|
||||
|
||||
This document outlines the bugs discovered in the code during the disassembly
|
||||
process.
|
||||
|
||||
# 1. Missing Animation Entering Water
|
||||
|
||||
When animating the player falling into water, a comparison was missed which
|
||||
causes `sprite_18` to not be used for the animation sequence. This bug also
|
||||
exists in the Japanese version as well as _Probotector_. It's clear from the
|
||||
code that there was an attempt to show `sprite_18` (frame 3 in the table below)
|
||||
as part of the animation sequence. `sprite_18` is the same sprite as when the
|
||||
player is in water and presses the d-pad down button.
|
||||
|
||||
| Frame 1 | Frame 2 | Frame 3 (missing) | Frame 4 |
|
||||
|-------------------------------------|-------------------------------------|-------------------------------------|-------------------------------------|
|
||||
|  |  |  |  |
|
||||
|
||||
|
||||
```
|
||||
@set_enter_water_sprite:
|
||||
...
|
||||
lda PLAYER_WATER_TIMER,x ; load water animation timer
|
||||
beq ... ; if timer has elapsed, branch
|
||||
cmp #$0c ; see if timer is greater than or equal to #$0c
|
||||
bcs ... ; branch if timer >= #$0c (keep current sprite)
|
||||
lda #$73 ; PLAYER_WATER_TIMER less than #$0c, update sprite
|
||||
sta PLAYER_SPRITE_CODE,x ; set player sprite (sprite_73) water splash
|
||||
cmp #$08 ; !(BUG?) always branch, doesn't compare to PLAYER_WATER_TIMER
|
||||
; but instead compares against #$73
|
||||
; no lda PLAYER_WATER_TIMER,x before this line, so part of splash animation is missing
|
||||
bcs ... ; branch using sprite_73
|
||||
lda #$18 ; dead code - a = #$18 (sprite_18) water splash/puddle
|
||||
sta PLAYER_SPRITE_CODE,x ; dead code - set player animation frame to water splash/puddle
|
||||
```
|
||||
|
||||
# 2. Accidental Sound Sample In Snow Field
|
||||
|
||||
After defeating the boss UFO (Guldaf), before calling the method
|
||||
`level_boss_defeated`, the developers forgot to load into the accumulator the
|
||||
appropriate sound code, usually #$57 for `sound_57`. So bytes in bank 7 are
|
||||
interpreted as an audio sample incorrectly and a short DMC channel audio clip is
|
||||
played.
|
||||
|
||||
`level_boss_defeated` uses whatever is in the accumulator as the sound code to
|
||||
play. Since the accumulator wasn't set, it has the previous value of #$ff (see
|
||||
`boss_ufo_routine_0b`). This is interpreted by the audio engine as a DMC sound
|
||||
code that doesn't exist.
|
||||
|
||||
The audio engine will incorrectly read bytes from `sound_table_00` as the dmc
|
||||
data below.
|
||||
|
||||
* sampling rate #$03 (5593.04 Hz), no loop
|
||||
* counter length #$77
|
||||
* offset #$97 --> $c000 + (#$97 * #$40) --> $e5c0
|
||||
* sample length #$19
|
||||
|
||||
The data that is interpreted as a DPCM-encoded DMC sample is $e5c0, which is
|
||||
in bank 7's `collision_box_codes_03` data. This bug does not exist in the
|
||||
Japanese version of the game because the `level_boss_defeated` is not
|
||||
responsible for playing the boss defeated sound. This bug does exist in
|
||||
_Probotector_.
|
||||
|
||||
Extracted sound sample: [sound_ff.mp3](attachments/sound_ff.mp3?raw=true)
|
||||
|
||||
Note that the boss defeated audio (`sound_55`) is still played because the enemy
|
||||
defeated routine is set to `boss_ufo_routine_09` (see
|
||||
`enemy_destroyed_routine_05`). `boss_ufo_routine_09` plays `sound_55`.
|
|
@ -0,0 +1,253 @@
|
|||
# Game Loop
|
||||
|
||||
The control flow of _Contra_ is dictated by the CPU memory address
|
||||
`GAME_ROUTINE_INDEX` ($18). You can think of the `game_routine_xx` methods like
|
||||
canal locks. As the game progresses, the flow moves to the next
|
||||
`game_routine`.
|
||||
|
||||
* `game_routine_00` and `game_routine_01` are for setting up the game intro
|
||||
and player select UI.
|
||||
* `game_routine_02` is for showing the demo.
|
||||
* `game_routine_03` loads and auto-plays the demo level
|
||||
* `game_routine_04` clears level memory and player-specific memory
|
||||
* `game_routine_05` is where majority of level logic is executed
|
||||
* `game_routine_06` runs at the end of the game after defeating the final boss
|
||||
alien
|
||||
|
||||
## game_routine_00
|
||||
This label is only run once to initialize the NES's graphics and CPU memory that
|
||||
is used for the introduction animation and player select.
|
||||
|
||||
* Set nametable tiles all to #$00 - `zero_out_nametables` (ROM: $1c9a2, MEM:
|
||||
Bank 7 $c9a2)
|
||||
* Load intro pattern table, nametable, and palette (`load_intro_graphics`)
|
||||
* Reset Komani code number of correct sequence inputs
|
||||
(`KONAMI_CODE_NUM_CORRECT`) to back to #$00
|
||||
* Initialize the horizontal scroll to #$01 for the sliding intro animation
|
||||
* Initialize the high byte of the delay timer that is used for waiting before
|
||||
showing the demo
|
||||
|
||||
## game_routine_01
|
||||
This label is executed once per frame repeatedly while the _Contra_ logo is
|
||||
scrolled across the screen (intro animation). This label also checks for the
|
||||
Konami code is checked. Once the logo is finished scrolling from right to left,
|
||||
the label will load the sprites needed to show Bill and Lance as well as the
|
||||
player select UI and cursor. Then the intro theme music will be played.
|
||||
Finally, this label waits for both timers to complete before either starting
|
||||
the game (if the player has made a selection), or show a demo level.
|
||||
|
||||
* Check for Konami code (`konami_input_check`)
|
||||
* Scroll intro graphic
|
||||
* This is executed repeatedly until scrolling stops, the the logic below is
|
||||
executed
|
||||
* Load assets after scrolling is complete (`game_routine_01_scroll_complete`)
|
||||
* Load player select menu
|
||||
* Play intro explosion sound
|
||||
* Load sprite for yellow falcon cursor and sprites for Bill and Lance
|
||||
* Wait for timer to complete before moving on to `game_routine_02`
|
||||
(`dec_theme_delay_check_user_input`)
|
||||
* The timer resets if select is pressed
|
||||
|
||||
## game_routine_02
|
||||
* Load demo level and plays the level
|
||||
* Stop level when demo timer elapsed and loads next level to demo (only
|
||||
levels 0-2)
|
||||
* Reset `GAME_ROUTINE_INDEX` to #$0 between demo levels to reshow intro
|
||||
scroll and player select
|
||||
|
||||
## game_routine_03
|
||||
This label is executed once the player has pressed start to begin a game while
|
||||
in `game_routine_02`. It simply ensures the intro theme is finished playing and
|
||||
then flashes the player selection until all timers are elapsed
|
||||
|
||||
* Wait for intro theme to finish playing
|
||||
* Flash "1 PLAYER" or "2 PLAYER" for a bit
|
||||
|
||||
## game_routine_04
|
||||
This label clears level memory and player-specific memory like number of lives,
|
||||
number of continues, as well as level header data.
|
||||
|
||||
* `init_score_player_lives` - clears memory addresses $0028 to $00f0 then
|
||||
`CPU_SPRITE_BUFFER` ($300) up to but not including `CPU_GRAPHICS_BUFFER`
|
||||
($700)
|
||||
|
||||
## game_routine_05
|
||||
This is the where the majority of the game logic is executed from.
|
||||
`game_routine_05` maintains its own separate set of routines for managing the
|
||||
level state. See Level Routines. There are #$0a level routines.
|
||||
|
||||
## game_routine_06
|
||||
This routine runs at the end of the game after defeating the final boss alien.
|
||||
Begins the `game_end_routine_XX` routine execution flow and runs through each
|
||||
routine
|
||||
|
||||
* `game_end_routine_00`
|
||||
* `game_end_routine_01`
|
||||
* `game_end_routine_02`
|
||||
* `game_end_routine_03`
|
||||
* `end_game_sequence_00`
|
||||
* `end_game_sequence_01`
|
||||
* `end_game_sequence_02`
|
||||
* `game_end_routine_04`
|
||||
* `game_end_routine_05`
|
||||
|
||||
# Level Routines
|
||||
`game_routine_05` maintains a `LEVEL_ROUTINE_INDEX` ($2c). This is an offset
|
||||
into the `level_routine_ptr_tbl` (ROM: $1ce35, MEM: Bank 7 $ce35). These
|
||||
routines manage the state of the level as the player progresses. These routines
|
||||
are not necessarily executed in order like a waterfall. There are #$0a level
|
||||
routines in total.
|
||||
|
||||
These routines manage
|
||||
* level initialization
|
||||
* showing score before starting a level
|
||||
* handle player input, including pause
|
||||
* level completion, advancing level
|
||||
* game over screen - continue / end
|
||||
* game over with no more continues
|
||||
* showing game ending sequence
|
||||
|
||||
Below is a mermaid diagram of the logic that dictates the state changes among
|
||||
the level routines.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
level_routine_00 --> level_routine_01
|
||||
level_routine_01 --> level_routine_02
|
||||
level_routine_02 --> level_routine_03
|
||||
level_routine_03 --> level_routine_04
|
||||
level_routine_04 --> |Level Complete|level_routine_08
|
||||
level_routine_04 --> |Game Over|level_routine_0a
|
||||
level_routine_05 --> |Level Complete|level_routine_00
|
||||
level_routine_05 --> |Complete Last Level/Game Over|level_routine_06
|
||||
level_routine_05 --> |Game Over No Continues|level_routine_07
|
||||
level_routine_06 --> |Continue/End Selected|level_routine_00
|
||||
level_routine_08 --> |Game Over|level_routine_0a
|
||||
level_routine_08 --> |Level Complete|level_routine_09
|
||||
level_routine_0a --> |Game Over Timer Expired|level_routine_05
|
||||
level_routine_09 --> |Level Complete|level_routine_05
|
||||
```
|
||||
|
||||
## level_routine_00
|
||||
This routine is responsible for loading the level header data into memory. This
|
||||
specifies things like what type of level (indoor/base, outdoor), where graphics
|
||||
data for the level are (super-tile pattern tiles, which super-tiles are on each
|
||||
screen, and palette data). Then the routine initializes many PPU write
|
||||
addresses, and loads the data which specifies the super-tiles on the first
|
||||
screen. Note that the actual super-tile pattern tiles aren't loaded into memory
|
||||
until `level_routine_03`.
|
||||
|
||||
* Load the level header data
|
||||
* Load palette for screen that displays number of lives remaining and level/
|
||||
stage name.
|
||||
* Initialize `BOSS_DEFEATED_FLAG` and `LEVEL_END_PLAYERS_ALIVE` to #$00
|
||||
* Call `init_ppu_write_screen_supertiles` to initialize PPU scroll offset,
|
||||
PPU write offsets and call `load_current_supertiles_screen_indexes` to
|
||||
decompress super-tiles indexes for level's screen to load into CPU memory at
|
||||
`LEVEL_SCREEN_SUPERTILES`
|
||||
* Loads bank 0 where the enemy routines are and initializes the memory address
|
||||
$80 to point to where the enemy routines are for the current level
|
||||
|
||||
## level_routine_01
|
||||
Responsible for displaying the number of lives remaining for players before the
|
||||
level loads. This routine does not execute when demoing (in demo mode). It is
|
||||
only executed once, whereas `level_routine_02` is executed repeatedly to flash
|
||||
the score. The number of lives does not flash.
|
||||
|
||||
## level_routine_02
|
||||
* Flash the score text until timer elapses
|
||||
* Load the theme music for the level `level_vert_scroll_and_song` when not in
|
||||
demo mode
|
||||
* Sets the vertical scroll
|
||||
|
||||
## level_routine_03
|
||||
Responsible for rendering the nametable super-tiles.
|
||||
|
||||
## level_routine_04
|
||||
* Sees if the player is pausing or un-pausing by reading input.
|
||||
* If the player is pausing, the pause sound is played and the game state is
|
||||
updated
|
||||
* If the player is paused, then the routine ends
|
||||
* Check if BOSS_DEFEATED_FLAG is set, the the current level routine is
|
||||
updated to `level_routine_08` and the current routine ends
|
||||
* Check if player(s) have entered game over state, and if so, updates the
|
||||
current level routine to `level_routine_0a` and the current routine ends
|
||||
* Run all enemy logic
|
||||
* Run soldier generation
|
||||
* Update palette for flashing palettes, see palette cycling documentation in
|
||||
`Graphics Documentation.md`
|
||||
* Load alternate graphics if necessary
|
||||
|
||||
## level_routine_05
|
||||
Handles when the the level is complete or ended due to game over. If it's not
|
||||
the last level, then the sets things up so the next level is loaded. If it's the
|
||||
last level, then configures things for the end of game sequences. During game
|
||||
over, after GAME_OVER_DELAY_TIMER elapses, level routine #$0a sets the next
|
||||
routine to be level_routine_05 so that the game over high score screen can be
|
||||
shown
|
||||
|
||||
* Clear memory $40 to $f0, then $300 to $600 (exclusively)
|
||||
* If game over
|
||||
* Load game over screen pattern table and show game over screen
|
||||
`show_game_over_screen`
|
||||
* Increment `CURRENT_LEVEL`
|
||||
* If the last level
|
||||
* Start game ending sequence `inc_routine_index_set_timer`
|
||||
* Increment `GAME_COMPLETION_COUNT` so next play through is more challenging
|
||||
* Reset `LEVEL_ROUTINE_INDEX` back to level_routine_00
|
||||
* If not the last level
|
||||
* Loads graphics for current score screen and level intro screen in
|
||||
`load_level_intro`
|
||||
* Increments `LEVEL_ROUTINE_INDEX` to go to level_routine_06
|
||||
`inc_routine_index_set_timer`
|
||||
|
||||
## level_routine_06
|
||||
This level routine is the game over screen routine. It is executed after the
|
||||
player has died. This routine shows the player scores, the high score. It also
|
||||
shows the text "GAME OVER" and gives the player the option to either "CONTINUE"
|
||||
or "END".
|
||||
|
||||
* Display score and "CONTINUE"/"END"
|
||||
* Handle player input to change cursor between "CONTINUE" and "END" (select
|
||||
button)
|
||||
* Handle player input to select either "CONTINUE" and "END" (start button)
|
||||
|
||||
## level_routine_07
|
||||
This routine is executed when there are no continues remaining. It shows the
|
||||
score and the text "GAME OVER" while waiting for the player to press start. Once
|
||||
the player presses start, the level routine is set to #$00
|
||||
|
||||
`level_routine_07` is executed after checking the number of remaining continues
|
||||
in `level_routine_05` (`@no_continues_remaining`).
|
||||
|
||||
## level_routine_08
|
||||
Boss destroy animation. Plays level end music
|
||||
|
||||
* Check if player(s) have entered game over state, and if so, updates the
|
||||
current level routine to `level_routine_0a` and the current routine ends.
|
||||
This seems unlikely as this check happens in `level_routine_04` as well.
|
||||
This same code is part of `level_routine_07`, and seems equally unlikely to
|
||||
happen for that level routine as well.
|
||||
|
||||
## level_routine_09
|
||||
This routine runs the appropriate level routine from the
|
||||
`end_level_sequence_ptr_tbl` table. After each sequence is finished executing,
|
||||
control is sent back to `level_routine_05`.
|
||||
|
||||
* `end_level_sequence_00`
|
||||
* Wait for the players to land if they were jumping
|
||||
* Set a small delay before `end_level_sequence_01` does its logic
|
||||
* `end_level_sequence_01`
|
||||
* Wait for timer to elapse
|
||||
* Run level-specific level routine ending animation from
|
||||
`end_of_lvl_lvl_routine_ptr_tbl`
|
||||
* `end_level_sequence_02`
|
||||
|
||||
## level_routine_0a
|
||||
This is the last level routine, but is executed as part of a game over sequence.
|
||||
After the player(s) get a game over, this is the routine that waits until
|
||||
`GAME_OVER_DELAY_TIMER` has elapsed before setting the level routine to #$05
|
||||
(`level_routine_05`) to reinitialize the level.
|
||||
|
||||
The `GAME_OVER_DELAY_TIMER` delay timer starts at #60 and is used to wait a bit
|
||||
before showing the player scores.
|
|
@ -0,0 +1,210 @@
|
|||
# Overview
|
||||
|
||||
Enemy logic is controlled by enemy routines, every enemy has a set of routines
|
||||
that it can execute. The list of routines per enemy is specified in the
|
||||
`*_routine_ptr_tbl` tables all in bank 7. Which enemies are in which screen of
|
||||
a level are specified in the `enemy_routine_ptr_tbl` (for shared enemies) or one
|
||||
of the 7 `enemy_routine_level_x` tables. Level 2 and level 4 share the same
|
||||
enemy routine `enemy_routine_level_2_4` table.
|
||||
|
||||
Upon execution of `level_routine_04` and `level_routine_0a`, every enemy is
|
||||
given the opportunity to execute logic specific to that enemy type. This is
|
||||
done by looping down #$f to #$0 through the `ENEMY_ROUTINE` memory addresses in
|
||||
the `exe_all_enemy_routine` method. Enemies are added to the end going down,
|
||||
i.e. they start at #$f and go down.
|
||||
|
||||
# Generation
|
||||
|
||||
Enemies can be generated in the level in one of two ways: random generation, and
|
||||
level-specific hard-coded locations.
|
||||
|
||||
# Level Enemies
|
||||
Each level defines hard-coded locations on each screen where an enemy will be
|
||||
generated. These enemies are always at that location in the level. This is
|
||||
defined in the `level_enemy_screen_ptr_ptr_tbl` in bank 2. There are #$08
|
||||
2-byte entries in this table, one for each level, e.g.
|
||||
`level_1_enemy_screen_ptr_tbl`. Each 2-byte entry is a memory address to
|
||||
another table of addresses, e.g. `level_1_enemy_screen_02`. Each entry here
|
||||
defines the enemies within a single screen of a level. Screen 0 does never has
|
||||
enemies, so the first entry in this table is associated to the second screen of
|
||||
the level. There is always one more entry for a level than there are screens.
|
||||
|
||||
## Data Structure
|
||||
|
||||
### Outdoor levels
|
||||
For a given screen, each enemy is defined with at least #$03 bytes. For
|
||||
example, the first enemy defined on `level_1_enemy_screen_00` is
|
||||
`.byte $10,$05,$60`. These three bytes define a soldier who runs left, but
|
||||
doesn't shoot. These bytes need to be broken up into bits to further understand
|
||||
their meaning.
|
||||
|
||||
```
|
||||
0001 0000 0000 0101 0110 0000
|
||||
XXXX XXXX RRTT TTTT YYYY YAAA
|
||||
```
|
||||
|
||||
* `X` - X offset
|
||||
* `R` - Repeat
|
||||
* `T` - Enemy Type
|
||||
* `Y` - Y Offset
|
||||
* `A` - Enemy Attribute
|
||||
|
||||
### Byte 1 - XX byte
|
||||
The first byte, #$10, from the example above specifies the x position of the
|
||||
enemy.
|
||||
|
||||
### Byte 2 - Repeat and Enemy Type
|
||||
The second byte, #$05, from the example above defines two things: repeat, and
|
||||
enemy type. The most significant 2 bits define the number of times to repeat
|
||||
an enemy, the least significant 6 bits define the enemy type. To see a list of
|
||||
all enemy types and what they are, see `Enemy Glossary.md`. For example, #$05
|
||||
has a repeat of 0 and a enemy type of #$05. #$05 is the soldier.
|
||||
|
||||
If the repeat value is 0, then the enemy is not repeated and will take a total
|
||||
of #$3 bytes. However, if there is a repeat, for each repetition, one more byte
|
||||
is added and has the same structure as the `Y Offset and Attribute` byte. This
|
||||
means an enemy with a repeated enemy will have the same XX position and the same
|
||||
type, but have its own Y position and attributes.
|
||||
|
||||
Here is an example of a screen enemy definition with a repeat
|
||||
|
||||
```
|
||||
level_1_enemy_screen_09:
|
||||
.byte $10,$43,$40,$b4 ; flying capsule (enemy type #$03), attribute: 000 (R), location: (#$10, #$40)
|
||||
; repeat: 1 [(y = #$b0, attr = 100)]
|
||||
.byte $e0,$07,$81 ; red turret (enemy type #$07), attribute: 001, location: (#$e0, #$80)
|
||||
.byte $ff
|
||||
```
|
||||
|
||||
### Byte 3 - Y Offset and Attribute
|
||||
The third byte, #$60, from the example above defines the vertical position of
|
||||
the enemy as well as that enemy's attributes. The #$05 most significant bits
|
||||
specify the vertical offset and the least significant 3 bits are for the
|
||||
attributes. Each enemy can use the 3 attribute bits however they see fit. For
|
||||
example, a soldier uses the attributes to know which way to start running, and
|
||||
whether or not the soldier fires bullets from their gun. For a detailed list of
|
||||
each enemy type and their attributes, see `Enemy Glossary.md`.
|
||||
|
||||
### Indoor/Base Levels
|
||||
|
||||
```
|
||||
XXXX YYYY CDTT TTTT AAAA AAAA
|
||||
```
|
||||
|
||||
* `X` - X offset
|
||||
* `Y` - Y Offset
|
||||
* `C` - Whether or not to add #$08 to Y position
|
||||
* `D` - Whether or not to add #$08 to X position
|
||||
* `T` - Enemy Type
|
||||
* `A` - Enemy Attribute
|
||||
|
||||
# Enemy Destroyed
|
||||
|
||||
When an enemy is determined to be destroyed, e.g. their `ENEMY_HP` has gone to 0
|
||||
after collision with a bullet, then the enemy routine for the active enemy is
|
||||
immediately adjusted to a routine index specified by
|
||||
`enemy_destroyed_routine_xx`. These are grouped in the
|
||||
`enemy_destroyed_routine_ptr_tbl`.
|
||||
|
||||
For example, when a soldier is destroyed, `enemy_destroyed_routine_01` specifies
|
||||
byte #$05 for the soldier. This corresponds to `soldier_routine_04`
|
||||
|
||||
# Soldier Generation
|
||||
|
||||
In addition to the hard-coded screen-specific enemies that appear in the same
|
||||
location every play through (specified in the `level_x_enemy_screen_xx` data
|
||||
structures). _Contra_ generates soldiers at regular intervals with slightly
|
||||
random enemy logic so that each play through has a different experience.
|
||||
|
||||
When playing a level, the game state is in `game_routine_05`, and specifically
|
||||
in `level_routine_04`. `level_routine_04` is run every frame. One part of
|
||||
`level_routine_04` is to run the logic to determine if an enemy soldier (enemy
|
||||
type #$05) should be created. This logic is in bank 2's
|
||||
`exe_soldier_generation` method.
|
||||
|
||||
`exe_soldier_generation` runs one of three soldier generation routines depending
|
||||
on the current value of `SOLDIER_GENERATION_ROUTINE`. Initially, this value is
|
||||
#$00 and `soldier_generation_00` is executed.
|
||||
|
||||
## soldier_generation_00
|
||||
`soldier_generation_00` initializes a level-specific timer that controls the
|
||||
speed of soldier generation. This timer is subsequently adjusted based on the
|
||||
number of times the game has been completed, and the player's current weapon
|
||||
strength. Every time the game has been completed (max of 3 times), #$28 is
|
||||
subtracted from the initial level-specific soldier generation timer.
|
||||
Additionally, the player weapon strength multiplied by #$05 is subtracted from
|
||||
the soldier generation timer.
|
||||
|
||||
For example, level 3 (waterfall) has an initial level-specific timer value of
|
||||
#$d8 (specified in the `level_soldier_generation_timer` table). If the player
|
||||
has beaten the game once and has a `PLAYER_WEAPON_STRENGTH` of #$03 (S weapon),
|
||||
then the computed soldier generation timer would be #$a1.
|
||||
|
||||
```
|
||||
#$a1 = #$d8 - (#$01 * #$28) - (#$05 * #$03)
|
||||
```
|
||||
|
||||
Soldier generation is disabled on the indoor/base levels (level 2 and level 4)
|
||||
along with level 8 (alien's lair). They are disabled by a value of #$00 being
|
||||
specified in the `level_soldier_generation_timer` table. For these levels, no
|
||||
other soldier generation routine will be run, only `soldier_generation_00`.
|
||||
|
||||
Once the soldier generation timer has been initialized and adjusted, the
|
||||
`SOLDIER_GENERATION_ROUTINE` is incremented so that the next game loop's
|
||||
`exe_soldier_generation` causes `soldier_generation_01` to execute.
|
||||
|
||||
## soldier_generation_01
|
||||
|
||||
`soldier_generation_01` is responsible for decrementing the soldier generation
|
||||
timer until it elapses. Then it is responsible for creating the soldier, if
|
||||
certain conditions are met. This includes randomizing the soldier's location and
|
||||
enemy attributes.
|
||||
|
||||
`soldier_generation_01` will first look at the current soldier generation timer,
|
||||
if the timer is not yet less than #$00, then the timer is decremented by #$02,
|
||||
unless the frame is scrolling on an odd frame number. Then the timer is only
|
||||
decremented by #$01.
|
||||
|
||||
Once the soldier generation timer has elapsed, the routine looks for an
|
||||
appropriate location to generate the soldier on the screen. Soldiers are
|
||||
always generated from the left or right edge of the screen. First the starting
|
||||
horizontal position is determined. This is essentially determined randomly by
|
||||
the current frame number and values in the `gen_soldier_initial_x_pos` table.
|
||||
The result will be either the left edge (#$0a) or the right edge (#$fa or #$fc).
|
||||
|
||||
There is an exception for level one. Until a larger number of soldiers have
|
||||
already been generated, soldiers will only appear from the right, probably to
|
||||
make the beginning of the game slightly easier.
|
||||
|
||||
Once the x position is decided, the routine will start looking for a vertical
|
||||
location that has a ground for the soldier to stand on. It does this in one of
|
||||
3 ways randomly to ensure soldiers are generated from multiple locations if
|
||||
possible. The 3 methods are from top of the screen to the bottom, from the
|
||||
bottom of the screen to the top, and from the player vertical position up to the
|
||||
top.
|
||||
|
||||
If a horizontal and vertical position is found where a soldier can be placed on
|
||||
the ground, then some memory is updated to specify the location and the soldier
|
||||
generation routine is incremented to `soldier_generation_02`.
|
||||
|
||||
## soldier_generation_02
|
||||
|
||||
At this point, a location is found for the soldier to generate.
|
||||
`soldier_generation_02` is responsible for actually initializing and creating
|
||||
the soldier. Some checks are performed to make sure it's appropriate to
|
||||
generate a soldier, for example, when `ENEMY_ATTACK_FLAG` is set to #$00 (off),
|
||||
then a soldier will not be generated. Other checks include that there are no
|
||||
solid blocks (collision code #$80) right in front of the soldier to generate,
|
||||
and that there is no player right next to the edge of the screen where the
|
||||
soldier would be generated from (this check doesn't happen after beating the
|
||||
game at least once). If any checks determine that the soldier should not be
|
||||
generated, then the routine resets the `SOLDIER_GENERATION_ROUTINE` back to #$00
|
||||
and stops.
|
||||
|
||||
To randomize the various behaviors of the generated soldiers, this routine will
|
||||
look up initial behavior from one of the `soldier_level_attributes_xx` tables
|
||||
based on the level. This will randomize the soldier direction, whether or not
|
||||
the soldier will shoot and how frequently, and a value specifying the
|
||||
probability of ultimately not generating the soldier. Finally, the soldier is
|
||||
generated and the values are moved into the standard enemy memory location
|
||||
addresses, creating the soldier.
|
|
@ -0,0 +1,517 @@
|
|||
# Overview
|
||||
|
||||
In the NES, a frame consists of the background and the sprites. Both the
|
||||
background and the sprites are composed of 8x8 pixel tiles from the pattern
|
||||
table. The background is 32 tiles wide and 30 tiles tall, for a total of
|
||||
256x240 pixels. This data structure is called the nametable. Each byte in the
|
||||
nametable references an index into the pattern table.
|
||||
|
||||
The pattern table is the data structure that describes what the 8x8 tiles in the
|
||||
nametable and sprites look like. Each pattern table entry is 16 bytes. This is
|
||||
the fundamental drawing block of the NES.
|
||||
|
||||
There are 4 nametables in total, but only 2 are written to and the remaining two
|
||||
are copies of the initial 2 nametables (mirroring). Additionally, each
|
||||
nametable has 64-bytes at the end to specify which palette is assigned each part
|
||||
of the nametable. This is called the attribute table. Each entry in the
|
||||
attribute table specifies the palette for a 4x4 set of tiles in the nametable.
|
||||
|
||||
Finally, there are sprites, which are drawn on top of the nametable and can be
|
||||
easily moved around the screen.
|
||||
|
||||
Addresses specified in this article include both the address that is used by the
|
||||
NES CPU to locate data, as well as the offset address in the PRG ROM where the
|
||||
referenced address is. Note that a ROM file has a #$10 byte iNES header that
|
||||
doesn't exist in actual PRG ROM. So when using the ROM address to look for the
|
||||
location in a NES ROM file, you will need to add an addition #$10 bytes to the
|
||||
ROM address.
|
||||
|
||||
## Terms
|
||||
|
||||
* Attribute Table - one of 2 64-byte tables (one for each nametable) that
|
||||
specifies which palettes to use for each part of the nametable
|
||||
* Background - A combination of the nametable and the attribute table
|
||||
* Graphic data - sections of compressed graphic data, typically pattern data,
|
||||
but can be nametable data, or even attribute data as well
|
||||
* Nametable - matrix of bytes referencing items in the pattern table. Can
|
||||
generally be thought of as the background.
|
||||
* Palette - a set of 4 colors that can be used for coloring a tile
|
||||
* Pattern table - a 16 byte object defining the look of an 8px by 8px tile
|
||||
* Screen - a set of super-tiles for a level that is 256 pixels wide
|
||||
* For horizontal levels, the size of a screen is 8x7 super-tiles, and for a
|
||||
vertical level it is 8x8 super-tiles
|
||||
* Sprite - a graphical element composed of pattern table tiles. Some
|
||||
documentation refers to these as 'objects'.
|
||||
* Sprites in _Contra_ are 8x16, meaning 2 pattern table tiles make up each
|
||||
component of a sprite.
|
||||
* Super-tile - a group of 4x4 set of tiles that are used to construct a level's
|
||||
nametable. This term isn't standard and was borrowed from part 2 of Allan
|
||||
Blomquist's 7-part series on _Contra_ called [Retro Game Internals](https://tomorrowcorporation.com/posts/retro-game-internals-contra-levels).
|
||||
Some people may refer to this as a meta tile.
|
||||
* Tile - an element of the nametable, represents an 8x8 pixel from the pattern
|
||||
table
|
||||
|
||||
# Background
|
||||
|
||||
Usually, for attribute, palette, and sprite data, the data is first loaded from
|
||||
the PRG ROM and then moved into CPU memory. Then, every frame that data is read
|
||||
from CPU memory into the PPU to display. Pattern table data is read directly
|
||||
from PRG ROM into the PPU.
|
||||
|
||||
The graphic data is in various places throughout the PRG ROM. For example
|
||||
(with the exception of the intro sequence and ending sequence), the palette
|
||||
colors are in bank 7, while the attribute table data is mostly in bank 3. For
|
||||
the game pattern table data, this is stored in compressed blocks in banks 2, 4,
|
||||
5, and 6. I refer to these blocks of data as 'graphic data', because while it
|
||||
mostly stores pattern table data, in some cases (intro, outro, and clearing
|
||||
screen) it does store nametable, and attribute data.
|
||||
|
||||
The graphic data in the game ROM are compressed using a encoding system known as
|
||||
[Run-length encoding](https://en.wikipedia.org/wiki/Run-length_encoding) (RLE).
|
||||
This algorithm is covered in another section in this document. As the graphic
|
||||
data are loaded into PPU memory from ROM memory, they are decompressed.
|
||||
|
||||
A level contains multiple "graphic data". These make up all the graphics (minus
|
||||
sprites) necessary for a level.
|
||||
|
||||
As part of loading graphics a level, the `load_level_graphic_data` label is
|
||||
executed, this looks up the appropriate graphic data from the
|
||||
`level_graphic_data_tbl` and then immediately proceeds to decompress and write
|
||||
the data to the PPU with label `write_graphic_data_to_ppu`.
|
||||
|
||||
As an example, the 7 graphic data used for level 1 `level_1_graphic_data`
|
||||
(ROM: $1c8fd, MEM: $c8fd) are as follows: #$03, #$13, #$19, #$1a, #$14, #$16,
|
||||
#$05, #$ff. Each byte is an offset entry into the `graphic_data_ptr_tbl` table.
|
||||
|
||||
`load_level_graphic_data` will load each of those graphic data offsets into the
|
||||
A register and then call `write_graphic_data_to_ppu`, which decompresses and
|
||||
writes the graphics into the PPU. This is repeated by `load_level_graphic_data`
|
||||
until the offset specified is #$ff, which signifies there are no more graphics
|
||||
data for the level to load.
|
||||
|
||||
While in theory you could specify the nametable with this same scheme, the
|
||||
level's nametables are not specified in these graphics data in general and
|
||||
instead graphics data is used only for pattern and attribute data. The only
|
||||
time the graphics data contains nametable data is for either blanking the
|
||||
nametable (`graphic_data_00` (ROM: $1cb36, MEM: Bank 7 $cb36)), or setting up
|
||||
the nametable for the intro and ending scenes (`graphic_data_02` and
|
||||
`graphic_data_18` respectively). Level nametable information is populated by
|
||||
"Super-Tiles". Read that section for more information.
|
||||
|
||||
In general, the left pattern table, PPU addresses [$0000-$1000), is used for
|
||||
storing pattern tiles associated with sprites. The right pattern table, PPU
|
||||
addresses [$1000-$2000), is used for storing pattern tiles associated with the
|
||||
nametable. This is useful to understand, because sprites are displayed with 2
|
||||
pattern table tiles, making up a 8x16 sprite. So, when viewing the PPU during
|
||||
a level with any debugging tools, it's appropriate to view the left pattern
|
||||
table as 8x16 sprites if that option is available.
|
||||
|
||||
## Graphics Data Locations
|
||||
|
||||
Below is a table of all graphic data locations in the ROM (excluding alternate
|
||||
graphics data).
|
||||
|
||||
The PPU Address ranges are specified in interval notation.
|
||||
* The square brackets [] represent 'closed interval' and the address is
|
||||
included in the range
|
||||
* The parentheses () represent 'open interval' and are not included in the
|
||||
range
|
||||
|
||||
| Label Name | Bank | Label In-Memory Address | PRG ROM Address | Graphics Data | PPU Addresses | Comments |
|
||||
|-------------------|------|-------------------------|-----------------|-----------------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `graphic_data_00` | 7 | $cb36 | $1cb36 | Both nametables + Both Attribute tables | [$2000-$2800) | Sets all Nametable + Attribute table data to #$00 |
|
||||
| `graphic_data_01` | 4 | $aa2d | $12a2d | Left and Right Pattern tables | [$0ce0-$1f80) | Used for intro screen, level title screens, and game over screens |
|
||||
| `graphic_data_02` | 2 | $9097 | $9097 | Nametable 0 + Attribute table 0 | [$2000-$2400) | Used for intro screen |
|
||||
| `graphic_data_03` | 4 | $8001 | $10001 | Left Pattern table | [$0000-$0680) | Used in every level. Contains Bill and Lance blocks, game over letters, lives medals, power-ups (SBFLRM), and explosions |
|
||||
| `graphic_data_04` | 4 | $85ae | $105ae | Left Pattern table | [$0680-$08c0) | |
|
||||
| `graphic_data_05` | 5 | $8001 | $14001 | Left and Right Pattern tables | [$09a0-$0a80), [$0dc0-$1200), [$1320-$1600), [$1bd0-$2000) | Character when immobile, and prone. Also has weapon zeppelin. writes to same PPU addresses as `graphic_data_07` and `graphic_data_0b` |
|
||||
| `graphic_data_06` | 4 | $99fc | $119fc | Left and Right Pattern tables | [$08c0-$1100) | Most Base graphics |
|
||||
| `graphic_data_07` | 5 | $8a61 | $14a61 | Left and Right Pattern tables | [$09a0-$0a80), [$0dc0-$1200), [$1320-$1600), [$1bd0-$2000) | Writes to same PPU addresses as `graphic_data_05` and `graphic_data_0b` |
|
||||
| `graphic_data_08` | 4 | $886c | $1086c | Left and Right Pattern tables | [$09a0-$2000) | |
|
||||
| `graphic_data_09` | 4 | $99cd | $119cd | Left Pattern table | [$0b00-$0b40) | |
|
||||
| `graphic_data_0a` | 4 | $a005 | $12005 | Right Pattern table | [$1100-$1520) | |
|
||||
| `graphic_data_0b` | 5 | $93e0 | $153e0 | Left and Right Pattern tables | [$09a0-$0a80), [$0dc0-$1200), [$1320-$1600), [$1bd0-$2000) | Writes to same PPU addresses as `graphic_data_05` and `graphic_data_07` |
|
||||
| `graphic_data_0c` | 6 | $8001 | $18001 | Left and Right Pattern tables | [$09a0-$0a80), [$0dc0-$0ee0), [$0fc0-$1200), [$1320-$2000) | |
|
||||
| `graphic_data_0d` | 6 | $8cdc | $18cdc | Left and Right Pattern tables | [$09a0-$0a80), [$0dc0-$0ee0), [$0fc0-$1200), [$1320-$2000) | |
|
||||
| `graphic_data_0e` | 6 | $9bd6 | $19bd6 | Left and Right Pattern tables | [$09a0-$2000) | |
|
||||
| `graphic_data_0f` | 4 | $a346 | $12346 | Right Pattern table | [$1520-$1600) | |
|
||||
| `graphic_data_10` | 4 | $a003 | $12003 | Right Pattern table | [$1a20-$2000) | Same as `graphic_data_11`, but horizontally flipped |
|
||||
| `graphic_data_11` | 4 | $a3e7 | $123e7 | Right Pattern table | [$1a20-$2000) | |
|
||||
| `graphic_data_12` | 4 | $a940 | $12940 | Right Pattern table | [$1b90-$1ca0) | |
|
||||
| `graphic_data_13` | 4 | $87a1 | $107a1 | Left Pattern table | [$08c0-$09a0) | |
|
||||
| `graphic_data_14` | 5 | $a814 | $16814 | Right Pattern table | [$1600-$1bd0) | |
|
||||
| `graphic_data_15` | 6 | $b07a | $1b07a | Left Pattern table | [$0ee0-$0fc0) | |
|
||||
| `graphic_data_16` | 6 | $b15c | $1b15c | Right Pattern table | [$1200-$1320) | |
|
||||
| `graphic_data_17` | 5 | $addf | $16ddf | Left and Right Pattern tables | [$0a60-$0fe0), [$15b0-$18a0) | |
|
||||
| `graphic_data_18` | 5 | $b30d | $1730d | Nametable 0 + Attribute table 0 | [$2000-$2400) | Used in ending scene |
|
||||
| `graphic_data_19` | 5 | $a31b | $1631b | Left Pattern table | [$0680-$08c0) | |
|
||||
| `graphic_data_1a` | 5 | $a500 | $16500 | Left Pattern table | [$0a80-$0dc0) | |
|
||||
|
||||
## Background Collision Data
|
||||
|
||||
The tiles in the pattern tables are ordered by which collision group they are
|
||||
in. There are 4 collision groups in Contra. The level header byte offsets
|
||||
#$09, #$0a, and #$ab define the boundaries of each collision group. For
|
||||
example, level 1's tile collision boundaries are $06, $f9, and $ff. This means
|
||||
|
||||
* Pattern tile index $00 has collision code 0 (empty). This is always true
|
||||
* Pattern tile indexes $01-$05 have collision code 1 (floor)
|
||||
* Pattern tile indexes $06-$f8 have collision code 0 (empty)
|
||||
* Pattern tile indexes $f9-$fe have collision code 2 (water)
|
||||
* Pattern tile index $ff has collision code 3 (solid)
|
||||
|
||||
For this reason, sometimes there are duplicate pattern table tiles, simply to
|
||||
create barriers.
|
||||
|
||||
The collision codes are defined as follows. Technically Collision Code 2 is
|
||||
treated as Collision code 0 #$00
|
||||
* Collision Code 0 (Empty) - tiles that have no collision data. Objects can
|
||||
freely pass through the tile
|
||||
* Collision Code 1 (Floor) - tiles that objects can land on when falling but
|
||||
pass through when moving left, up and right. Can fall through (drop down)
|
||||
with d-pad down, plus A only when there is another non-empty object below
|
||||
(or on vertical level).
|
||||
* Collision Code 2 (Water) - similar to code 1, but for water, prevents
|
||||
jumping, and causes player/enemy sprites to show only top half
|
||||
* Collision Code 3 (Solid) - The tile is solid and cannot be gone through
|
||||
regardless of directions or side.
|
||||
|
||||
For outdoor levels, the nametable collision codes are stored in CPU memory
|
||||
`BG_COLLISION_DATA` [$0680-$0700). Nametable collision codes are only stored
|
||||
for every other column and every other row of the nametable. Since a super-tile
|
||||
is 4 columns wide and 4 columns tall, a single super-tile has 4 collision code
|
||||
points: top left, middle left, middle top, and middle middle.
|
||||
|
||||
Each byte in `BG_COLLISION_DATA` can be broken down into 4 collision codes.
|
||||
Every 2 bits represents the collision code (0, 1, 2, or 3) for one of the 4
|
||||
points in the super-tile. The first 8 bytes are for the top rows of collision
|
||||
points for a single nametable row. The next 8 bytes are the middle collision
|
||||
points. This pattern repeats until all of the first nametable data is specified
|
||||
and then is repeated for the 2nd nametable. For a given background collision
|
||||
entry, add #$04 to determine the next row down. The following is an example of
|
||||
the byte offsets from `BG_COLLISION_DATA` for the top left nametable. Note that
|
||||
next nametable starts at offset #$40 and not #$38.
|
||||
|
||||
Note that this is similar in structure to the attribute table.
|
||||
* Each byte contains data for a #$0f (16) nametable tiles
|
||||
* Each 2 bits within the byte specifies data for 4 nametable table tiles (2x2
|
||||
area).
|
||||
|
||||
However, the offsets within the structure refer to different areas of the
|
||||
nametable. Unlike the attribute table, where each byte specifies a 4x4 area of
|
||||
pattern table tiles, the background collision data byte specifies data for a 2x8
|
||||
area of pattern table tiles. The table below shows how the background collision
|
||||
data is arranged on the nametable.
|
||||
|
||||
```
|
||||
00 00 00 00 01 01 01 01 02 02 02 02 03 03 03 03
|
||||
04 04 04 04 05 05 05 05 06 06 06 06 07 07 07 07
|
||||
08 08 08 08 09 09 09 09 0a 0a 0a 0a 0b 0b 0b 0b
|
||||
0c 0c 0c 0c 0d 0d 0d 0d 0e 0e 0e 0e 0f 0f 0f 0f
|
||||
10 10 10 10 11 11 11 11 12 12 12 12 13 13 13 13
|
||||
14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17
|
||||
18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 21
|
||||
22 22 22 22 23 23 23 23 24 24 24 24 25 25 25 25
|
||||
26 26 26 26 27 27 27 27 28 28 28 28 29 29 29 29
|
||||
30 30 30 30 31 31 31 31 32 32 32 32 33 33 33 33
|
||||
34 34 34 34 35 35 35 35 36 36 36 36 37 37 37 37
|
||||
```
|
||||
|
||||
Indoor/base levels do not really rely on the `BG_COLLISION_DATA` for background
|
||||
collision detection. The level header byte offsets #$09, #$0a, and #$ab for
|
||||
the indoor/base levels are #$00, #$ff, #$ff, which means all tiles are collision
|
||||
code 0, i.e. empty. The player is prevented from walking past the left and
|
||||
right on indoor/base levels walls manually in the code, e.g. see
|
||||
`level_right_edge_x_pos_tbl`, and `level_left_edge_x_pos_tbl`.
|
||||
|
||||
## Super-Tiles
|
||||
_Contra_'s level nametable data is made up of "Super-tiles". Each super-tile is
|
||||
#$10 bytes long and made of 4 rows of 4 pattern table entries each. Super-tiles
|
||||
are not RLE-encoded. A super-tile is how the level nametable's are defined and
|
||||
can be thought of as smallest unit of art that can be used for nametable drawing
|
||||
in a level. This isn't technically true, because a nametable can be updated
|
||||
after it's drawn for certain animations (claw, fire beam, etc). For horizontal
|
||||
levels, a background is composed of #$38 super-tiles (8x7). For the vertical
|
||||
level, a background is composed of #$40 super-tiles (8x8).
|
||||
|
||||
Within a super-tile, there are 4 2x2 sections each #$4 bytes long.
|
||||
|
||||
The location of the pattern table tiles of each super-tile pattern is specified
|
||||
in the 2-byte level header data byte #$04 (`LEVEL_SUPERTILE_DATA_PTR`). For
|
||||
example, level 1's super-tile data is defined at `level_1_supertile_data`
|
||||
(ROM: $c001, MEM: Bank 3 $8001). Each #$10 bytes is a single super-tile. Level
|
||||
1 has a total of #$3b distinct super-tiles.
|
||||
|
||||
For each level, the 2-byte level header offset byte #$02
|
||||
(`LEVEL_SCREEN_SUPERTILES_PTR`) specifies which super-tiles are part of each
|
||||
'screen'. A 'screen' is the set of #$38 or #$40 super-tiles that make up a
|
||||
single background. For an example, level 1's screen super-tile index data is
|
||||
located at `level_1_supertiles_screen_ptr_table`
|
||||
(ROM: $8001, MEM: Bank 2 $8001). Each entry specifies the super-tiles used for a
|
||||
single screen. The super-tiles used in the first screen of level 1 are
|
||||
specified at `level_1_supertiles_screen_00` (ROM: $801d, MEM: Bank 2 $801d).
|
||||
This data is compressed with RLE-encoding
|
||||
|
||||
Super-tiles indexes are stored in CPU memory and referenced when drawing the
|
||||
background. 2 screens of super-tile indexes are stored in memory at a time.
|
||||
This is to support scrolling. The super-tiles indexes for the screens are
|
||||
loaded into CPU memory at location $0600 (`LEVEL_SCREEN_SUPERTILES`) by
|
||||
`load_supertiles_screen_indexes` and used when writing the pattern table tile
|
||||
data to CPU memory by `load_level_supertile_data`. The writing of the
|
||||
super-tiles to the nametable takes place incrementally, one nametable column at
|
||||
a time.
|
||||
|
||||
# Sprites
|
||||
Sprites are movable objects that are composed of elements from the pattern
|
||||
table. This documentation refers to a group of sprites together as a single
|
||||
sprite. For example, Bill is composed of 5 sprites, but will spoken about as
|
||||
a single sprite. Sprites in _Contra_ are composed of 2-block pattern table
|
||||
entries stacked on top of each other, making the smallest part of a sprite 8x16.
|
||||
Like most NES games, _Contra_ does not write sprite data directly to the PPU
|
||||
byte by byte via the `$2004` (OAMDATA) address but instead utilizes the `$4114`
|
||||
(OAMDMA) address. OAMDMA stands for Object Attribute Memory Direct Memory
|
||||
Access. The OAMDMA address is set to #$02, which tells the PPU to load sprite
|
||||
data from the address range `$0200` to `$02ff` inclusively every frame.
|
||||
|
||||
All of the sprites in _Contra_ are in bank 1 starting with `sprite_00` (ROM:
|
||||
$71ce, MEM: Bank 1 $b1ce). Each sprite can be referred to by an index starting
|
||||
from 00. I refer to these as the "sprite code" or "sprite number". There are
|
||||
two large pointer tables that point to each sprite address: `sprite_ptr_tbl_0`
|
||||
and `sprite_ptr_tbl_1`.
|
||||
|
||||
## Sprite CPU Buffer
|
||||
Sprites are drawn on the screen every frame after the current game routine is
|
||||
run. This done in the `draw_sprites` label (ROM: $6e97, MEM: Bank 1 $ae97).
|
||||
`draw_sprites` uses data stored in `$0300` to `$0367` inclusively. I refer to
|
||||
this area of memory as the sprite cpu buffer. _Contra_ supports a maximum of
|
||||
#$19 sprites on screen simultaneously. However, the certain addresses are
|
||||
reserved for "player" sprites, whereas other addresses are reserved for enemy
|
||||
sprites. The list below shows how the sprite CPU buffer is partitioned
|
||||
|
||||
| Address Range | Length | Alias | Usage |
|
||||
|--------------------|------------|-------------------|------------------------------------------------------------------------------------|
|
||||
| `$0300` to `0309` | #$0a bytes | PLAYER_SPRITES | sprite number for player and player bullets (`sprite_ptr_tbl` or `sprite_ptr_tbl_1`|
|
||||
| `$030a` to `0319` | #$10 bytes | ENEMY_SPRITES | enemy sprite number (`sprite_ptr_tbl` or `sprite_ptr_tbl_1`) |
|
||||
| `$031a` to `0323` | #$0a bytes | SPRITE_Y_POS | y position on screen of the each sprite |
|
||||
| `$0324` to `$0333` | #$10 bytes | ENEMY_Y_POS | y position of each enemy sprite |
|
||||
| `$0334` to `$033d` | #$0a bytes | SPRITE_X_POS | x position on screen of the each sprite |
|
||||
| `$033e` to `$034d` | #$10 bytes | ENEMY_X_POS | x position on screen of the each enemy sprite |
|
||||
| `$034e` to `$0357` | #$0a bytes | SPRITE_ATTR | sprite attributes (palette, whether to flip horizontally, vertically) |
|
||||
| `$0358` to `$0367` | #$10 bytes | ENEMY_SPRITE_ATTR | sprite attributes (palette, whether to flip horizontally, vertically) |
|
||||
|
||||
`draw_sprites` loops from #$19 to #$00 and uses the data from the sprite CPU
|
||||
buffer to populate the #$100 bytes `$0200` to `$02ff`. This data is then moved
|
||||
to the PPU with OAMDMA when in vertical blank `nmi_start`.
|
||||
|
||||
One neat trick that's done when loading data from the sprite CPU buffer to the
|
||||
`$0200` page memory is that every time the `draw_sprites` label is called, an
|
||||
offset of #$4c is added to the write address. This causes the sprites to be
|
||||
placed in a different starting spot in the `$0200` page every frame. This is
|
||||
done to work around a limitation of the NES that restricts a scan line from
|
||||
having more than 8 sprites. By moving the sprites around in memory, if there are
|
||||
too many sprites on a scan line, different sprites will be drawn on different
|
||||
frames. The player shouldn't be able to detect a sprite missing if drawn quickly
|
||||
enough.
|
||||
|
||||
When filling the OAMDMA data, the sprite tiles aren't written sequently, but
|
||||
rather spaced every 49 sprite tiles apart (#$c4 bytes) wrapping around. I'm not
|
||||
sure why this was done. Perhaps it has to do with concern about dynamic RAM
|
||||
decay that OAM has.
|
||||
|
||||
## Sprite Number Encoding
|
||||
The sprite data in bank 1 is encoded. Except when #$fe, the first byte
|
||||
specifies the number of entries in the sprite. There there are that many groups
|
||||
of 4-bytes. Each #$4 bytes specify two tiles that are stacked vertically.
|
||||
Except when the first byte is #$80, these four bytes follow the PPU OAM byte
|
||||
specification. For details on this data structure,
|
||||
[NES Dev Wiki](https://wiki.nesdev.org/w/index.php?title=PPU_OAM) has a good
|
||||
article on this. These specifics are also outlined below.
|
||||
|
||||
* Byte 0 specifies the y position of the tile relative to where the sprite is
|
||||
placed on the screen. This can be negative.
|
||||
* If Byte 0 is #$80, then it is a signal to change the read address. The
|
||||
next two bytes specify the CPU address to move to. This functionality
|
||||
allows sprites to share pieces from other sprites.
|
||||
* Byte 1 is the tile number, i.e. which tile in the pattern table to use.
|
||||
* Since sprites are built from 8x16 components, and byte 1 only specifies
|
||||
the first tile, the second tile stacked immediately below this tile is
|
||||
always the next one in the pattern table. For example, if byte 1 is #$05,
|
||||
then the two tiles will be #$05 and #$06. If the tile specified is even,
|
||||
then the tile
|
||||
* All of _Contra_'s sprites are composed of even bytes, meaning the tile is
|
||||
pulled from the left pattern table ($0000-$0fff).
|
||||
* Byte 2 specifies various attributes that can be used to modify the tiles
|
||||
* Bit 7 (`x... ....`) specifies vertical flip
|
||||
* When the vertical flip bit is set, the top and bottom tiles are drawn
|
||||
vertically flipped. Also, the top tile is drawn at the bottom and the
|
||||
bottom tile is drawn at the top.
|
||||
* Bit 6 (`.x.. ....`) specifies horizontal flip
|
||||
* Whether or not to flip the tile horizontally
|
||||
* Bit 5 (`..x. ....`) specifies drawing priority
|
||||
* 0 = in front of background; 1 = behind background
|
||||
* Bits 1 and 0 (`.... ..xx`) specify the palette code
|
||||
* These bits specify the palette to use for the sprite
|
||||
* Byte 3 specifies the x position of the tile relative to where the sprite is
|
||||
placed on the screen. This can be negative.
|
||||
|
||||
When the first byte of the sprite code is #$fe, the sprite is a "small" sprite.
|
||||
Small sprites are composed of #$3 bytes, including the #$fe byte. Small sprites
|
||||
always have their X position set to #$fc (-4) and their Y position set to #$f8
|
||||
(-8).
|
||||
|
||||
* Byte 0 is #$fe and signifies the sprite is a small sprite
|
||||
* Byte 1 specifies the tile number
|
||||
* Byte 2 is the sprite attributes
|
||||
|
||||
Note: Sprites are also referred to as objects. This explains the names of some
|
||||
addresses in NES like Object Attribute Memory (OAM).
|
||||
|
||||
# Palette
|
||||
The NES supports 4 palettes for the nametables (background) and 4 palettes for
|
||||
sprites. A palette is a set of 3 colors. For nametables, one palette is shared
|
||||
for each square 16x16 set of 4 pattern table tiles. For sprites, each tile in a
|
||||
sprite has its own palette.
|
||||
|
||||
Defining which palette is assigned to which super-tile is located in bank 3. The
|
||||
level headers specify the location in bank 3 of where the super-tile palette
|
||||
data is defined (see `LEVEL_SUPERTILE_PALETTE_DATA`). This data is used to
|
||||
populate the attribute table.
|
||||
|
||||
For each sprite tile, the palette used is specified in the two least significant
|
||||
bits of byte 2 of the sprite OAM data. For details about how sprites are stored
|
||||
in the PRG ROM, see Sprite Number Encoding.
|
||||
|
||||
The initial colors that are associated with each palette are specified in the
|
||||
level header starting at byte offset 15 (see `LEVEL_PALETTE_INDEX`). Each of
|
||||
the next 8 bytes specify an entry into `game_palettes`. The first 4 bytes are
|
||||
the nametable palette colors, and the next 4 bytes are the sprite palette
|
||||
colors. Each entry in `game_palettes` specifies the 3 colors that make up the
|
||||
palette.
|
||||
|
||||
## Fade-In Effect
|
||||
_Contra_ supports a fade in effect for nametable palettes. This effect is used
|
||||
various parts of the game like base/indoor boss screens, dragon, and boss ufo.
|
||||
This is implemented by adjusting the specified nametable palette colors by an
|
||||
amount specified in `palette_shift_amount_tbl` when `BG_PALETTE_ADJ_TIMER` is
|
||||
specified and within range [#$01-#$09]. To utilize this effect, the code sets
|
||||
the `BG_PALETTE_ADJ_TIMER` to a value larger than #$09. Every frame, this value
|
||||
is decremented. When loading the palette colors for the frame, if
|
||||
`BG_PALETTE_ADJ_TIMER` is greater than #$09, then the palette color will be
|
||||
black, but once the value is in range, the palette colors will start to be
|
||||
modified. For example, to create the fade-in effect of the base/indoor boss
|
||||
screen background, the value of `BG_PALETTE_ADJ_TIMER` is set to #$0c. This
|
||||
gives #$03 frames of black, before the fade in effect starts.
|
||||
|
||||
## Cycling
|
||||
_Contra_ cycles the colors within nametable palettes during game play. The
|
||||
palettes specified by the nametable don't change, but rather the colors within
|
||||
the palettes are changed. By swapping out the colors, _Contra_ can create
|
||||
animations like blinking stars, waves in water, waterfalls, etc. Each level
|
||||
natively supports having its 4th nametable (background) palette cycle through up
|
||||
to 4 different sets of colors, where each set of colors is represented by an
|
||||
index into the `game_palettes` table. The specific indexes to cycle through are
|
||||
in the level headers (`LEVEL_PALETTE_CYCLE_INDEXES`). Level 3 (Waterfall) is
|
||||
special in that it only cycles among 3 different sets of colors instead of 4
|
||||
(see `lvl_palette_animation_count`).
|
||||
|
||||
Additionally, all levels cycle their 3rd nametable palette colors through a
|
||||
single, shared set of palette indexes (`level_palette_2_index`). This cycle of
|
||||
colors is a flashing red. It is used so that enemies have flashing red lights.
|
||||
|
||||
Finally, there are a few special nametable cycles, like for indoor (base) boss
|
||||
screens (`indoor_boss_palette_2_index`), and ending screen
|
||||
(`ending_palette_2_index`).
|
||||
|
||||
Typically sprites don't have any palette color cycling support natively, but
|
||||
when invincible, the player sprites will cycle the palette colors.
|
||||
|
||||
Like sprite data and nametable data, the palette color data is loaded from PRG
|
||||
ROM into CPU memory first. Then every frame, that data is read and loaded into
|
||||
PPU memory. Every 8 frames, the `LEVEL_PALETTE_CYCLE` is incremented so that the
|
||||
palettes colors are changed.
|
||||
|
||||
Every frame, the `load_palette_indexes` looks at the current
|
||||
`LEVEL_PALETTE_CYCLE` and uses that to determine the appropriate indexes into
|
||||
`game_palettes` to store into CPU memory $52 and $53 (`LEVEL_PALETTE_INDEX`).
|
||||
Then `load_palette_colors_to_cpu` will look up the colors from the
|
||||
`game_palettes` table and store the colors in CPU memory starting at $07c0
|
||||
(`PALETTE_CPU_BUFFER`). Then in `write_palette_colors_to_ppu`, the data is read
|
||||
from $07c0 to $07df and written to the PPU starting at $3f00
|
||||
|
||||
# _Contra_ Compression
|
||||
|
||||
The graphic data for _Contra_ is largely compressed with a relatively basic
|
||||
compression algorithm known as
|
||||
[Run-length encoding](https://en.wikipedia.org/wiki/Run-length_encoding) (RLE).
|
||||
|
||||
The idea of this algorithm is that the graphics data will have long "runs" of
|
||||
repeated information. So instead of specifying the same byte over and over, it
|
||||
can be encoded so that the number of repetitions is specified. Essentially,
|
||||
instead of
|
||||
* #$00 #$00 #$00 #$00 #$00 #$00 #$00 #$00 #$00
|
||||
It can be represented like
|
||||
* #$89 #$00
|
||||
|
||||
Interestingly, this algorithm is implemented twice in _Contra_: once for pattern
|
||||
table tiles (`write_graphic_data_to_ppu` and once again for super-tile
|
||||
screen indexes (`load_supertiles_screen_indexes`).
|
||||
|
||||
## Pattern Table Decompression
|
||||
The graphic data is loaded from ROM directly to the PPU. Not only is the
|
||||
graphic data compressed, it also includes command bytes that specify where in
|
||||
the PPU to write the data to. This section outlines how the data is read and
|
||||
decompressed. This logic is all handled by the `write_graphic_data_to_ppu`
|
||||
label in bank 7.
|
||||
|
||||
The graphic data are simply parts of the ROM that contain bytes. The first two
|
||||
bytes of any graphic data are the starting address of where to write the graphic
|
||||
data to on the PPU. For example, the first two bytes of `graphic_data_04` are
|
||||
#$80, #$06. This means that the graphics data will be written to the PPU
|
||||
starting at PPU address $0680 (pattern table).
|
||||
|
||||
After the first two bytes are read, the following algorithm reads the graphics
|
||||
data section. When reading the graphic data, if the most significant bit of the
|
||||
graphic data byte is set, i.e 1, then the byte is treated as a command byte.
|
||||
There are 3 command byte types
|
||||
|
||||
* #$ff - specifies the end of graphic data
|
||||
* #$7f - tells the algorithm to change PPU write address to the next 2 bytes
|
||||
specified.
|
||||
* bit 7 set - any byte larger than #$7f, or alternatively, any byte that has
|
||||
its most significant bit set to 1 is treated as a RLE command to write the
|
||||
next byte of data multiple times to the PPU. This excludes #$ff, which
|
||||
always means end of graphic data
|
||||
|
||||
Level section data not encoded exactly the same way
|
||||
`level_1_supertiles_screen_ptr_table`
|
||||
|
||||
# Quick Reference
|
||||
|
||||
## Graphic Locations
|
||||
* `level_graphic_data_tbl` (ROM: $1c8e3, MEM: Bank 7 $c8e3) specifies which
|
||||
pattern table data is loaded for a level, indexes into `graphic_data_ptr_tbl`
|
||||
* `graphic_data_ptr_tbl` (ROM: $1c950, MEM: $c950) specifies locations in banks
|
||||
2, 4, 5, 6, or 7 of RLE compressed graphic data (typically pattern table
|
||||
data)
|
||||
* `LEVEL_SUPERTILE_DATA_PTR` is specified in the level headers and points to
|
||||
the compressed makeup of the level's super-tiles, entries are into the
|
||||
pattern table. It is a 2-byte pointer stored in the CPU at address $44.
|
||||
* `LEVEL_SCREEN_SUPERTILES_PTR` is specified in the level headers and it
|
||||
points to a table whose entries are pointers to data specifying which
|
||||
super-tiles are loaded in each screen of the level. That data is
|
||||
RLE-compressed. It is a 2-byte pointer stored in the CPU at address $42.
|
||||
|
||||
## Important Labels
|
||||
* `load_level_graphic_data` (ROM: $1c8c6, MEM: Bank 7 $c8c6) decompresses and
|
||||
loads the "Graphic data" for the current level
|
||||
* `load_current_supertiles_screen_indexes` (ROM: $1e169, MEM: Bank 7 $e169)
|
||||
decompresses and loads the super-tile indexes into memory
|
||||
* `load_level_supertile_data` (ROM: $1df48, MEM: Bank 7 $df48) decompresses
|
||||
and loads the super-tiles from the pattern table
|
||||
* `write_graphic_data_to_ppu` (ROM: $1c9a1, MEM: Bank 7 $c9a1) loads and
|
||||
decompresses the entire graphic data specified by the A register as offset
|
||||
into `graphic_data_ptr_tbl`
|
||||
* `@flush_cpu_graphics_buffer` (ROM: $1cc3f, MEM: $cc3f) writes all the data in
|
||||
the CPU graphics buffer to the PPU
|
||||
* `draw_sprites` (ROM: $6e97, MEM: Bank 1 $ae97) populates $0200-$02ff with
|
||||
sprite data for OAMDMA.
|
|
@ -0,0 +1,178 @@
|
|||
# How Contra Prints the High Score
|
||||
|
||||
## Overview
|
||||
There are 3 scores stored in CPU memory for _Contra_: high score, player 1
|
||||
score, and player 2 score. Each score is 2 bytes and stored in addresses
|
||||
0x07e0-0x07e1, 0x07e2-0x07e3, and 0x07e4-0x07e5 respectively. These 2-byte
|
||||
scores are little-endian, i.e. the least significant byte is the lower-addressed
|
||||
byte. All scores have 00 appended when displayed, for example a score of 100
|
||||
will show as 10000 on the screen. The default initial high score stored in
|
||||
0x07e2 is 0xc8, or 200 in decimal. This will show as 20000. The exception to
|
||||
this is a score of 0. 0 points will show as 0 and not 000.
|
||||
|
||||
The logic to display the score on the screen is non-trivial and worth properly
|
||||
documenting. To display the score on the screen, the code needs to convert an
|
||||
in-memory hexadecimal number to a decimal number.
|
||||
|
||||
At a high level, _Contra_ recursively calculates each decimal digit to display
|
||||
one at a time from right to left. If the player's score is 123 in decimal, or
|
||||
0111 1011 in binary. _Contra_ will call the same routine 3 times, each time
|
||||
producing the next digit in the score from right to left: 3, then 2, then 1.
|
||||
|
||||
|Call # | Rest | Right Digit |
|
||||
|-------|------|-------------|
|
||||
| 1 | 12 | 3 |
|
||||
| 2 | 1 | 2 |
|
||||
| 3 | 0 | 1 |
|
||||
|
||||
As you can see, each row can be read as Score = (10 * Rest) + Right_Digit
|
||||
The Rest column can be conceptually though of as 10 times larger than what is
|
||||
stored in memory. So 12 really means 120, and 1 really means 10. This will make
|
||||
conceptualizing the algorithm easier.
|
||||
|
||||
## Examples
|
||||
This document's examples are simplified slightly in that these examples are
|
||||
using 1-byte scores and not 2-byte scores like _Contra_. The source code shown
|
||||
works against 2-byte scores.
|
||||
|
||||
### Example of Routine
|
||||
As shown above, the routine is called a number of times, each time retrieving
|
||||
the next digit of the score to display. Below is a step-by-step walk through of
|
||||
calculating a single digit of the score to display.
|
||||
|
||||
Score (0x01): 0x7b 0111 1011 (123 in decimal)
|
||||
|
||||
Right Digit (0x02): 0x00 0000 0000 (0)
|
||||
|
||||
|Shift # | Rest | Right Digit | Input | Explanation |
|
||||
|--------|-----------|-------------|----------|------------------------------|
|
||||
| 1 | | 0 | 111 1011 | left shift |
|
||||
| 2 | | 01 | 111 011 | left shift |
|
||||
| 3 | | 011 | 11 011 | left shift |
|
||||
| 4 | | 0111 | 1 011 | left shift |
|
||||
| 5 | | 1111 | 011 | left shift |
|
||||
| | | 0101 | 011 | subtract 10 from right digit |
|
||||
| 6 | 1 | 1010 | 11 | shift left, mark 1 into Rest |
|
||||
| | 1 | 0000 | 11 | subtract 10 |
|
||||
| 7 | 11 | 0001 | 1 | shift left, mark 1 into Rest |
|
||||
| 8 | 110 | 0011 | | shift left |
|
||||
| 9 | 1100 | 0011 | | shift left Rest only |
|
||||
|
||||
Rest (0x01): 0000 1100 (12 decimal)
|
||||
|
||||
Right Digit (0x02): 0000 0011 (3 decimal)
|
||||
|
||||
The way _Contra_ determines the right-most digit recursively is very
|
||||
interesting. Every time the algorithm shifts left, the input is passed through
|
||||
the "Right Digit" column which will track when the single-digit is more than 10.
|
||||
Every time the right digit's column is more than 10, 10 is subtracted. On the
|
||||
subsequent left shift, 1 is placed on the right-most bit of the Rest column.
|
||||
That 1 signifies that a subtraction of 10 was performed. Remember that the Rest
|
||||
column's value is actually 10 times larger, e.g. 12 really means 120. So when
|
||||
the 1 is carried to the "Rest" column, it is really a 10 being carried, which
|
||||
matches what was subtracted from the "Right Digit" column.
|
||||
|
||||
### Memory-accurate Example With a 1-byte Score
|
||||
While the previous example helps understand at a conceptual level, it hides some
|
||||
of the technical ingenuity. _Contra_ doesn't split the score ("Input") into
|
||||
"Right Digit" and "Rest". _Contra_ actually shifts the "Rest" back into the
|
||||
"Input". This is useful because it allows the routine to be called
|
||||
consecutively until there are no more digits to display.
|
||||
|
||||
Again, _Contra_ uses 2-byte scores, but if _Contra_ had 1-byte scores, this is
|
||||
how the algorithm would work, while being memory accurate. 0x01 is shifted into
|
||||
0x02. Right Digit (0x02) is always initialized to zero.
|
||||
|
||||
Score (0x01): 0x7b 0111 1011 (123)
|
||||
|
||||
Right Digit (0x02): 0x00 0000 0000 (0)
|
||||
|
||||
|Shift # | 0x01 | 0x02 | Explanation |
|
||||
|--------|-------------|-------------|------------------------------|
|
||||
| 1 | 1111 0110 | 0000 0000 | left shift 0x01 only |
|
||||
| 2 | 1110 1100 | 0000 0001 | left shift 0x02, 0x01 |
|
||||
| 3 | 1101 1000 | 0000 0011 | left shift 0x02, 0x01 |
|
||||
| 4 | 1011 0000 | 0000 0111 | left shift 0x02, 0x01 |
|
||||
| 5 | 0110 0000 | 0000 1111 | left shift 0x02, 0x01 |
|
||||
| | 0110 0000 | 0000 0101 | subtract 10 |
|
||||
| 6 | 1100 0001 | 0000 1010 | shift left, mark 1 into Rest |
|
||||
| | 1100 0001 | 0000 0000 | subtract 10 |
|
||||
| 7 | 1000 0011 | 0000 0001 | shift left, mark 1 into Rest |
|
||||
| 8 | 0000 0110 | 0000 0011 | shift left 0x02, 0x01 |
|
||||
| 9 | 0000 1100 | 0000 0011 | shift left 0x01 only |
|
||||
|
||||
Rest (0x01): 0000 1100 (12 decimal)
|
||||
|
||||
Right Digit (0x02): 0000 0011 (3 decimal)
|
||||
|
||||
You can see that after this routine is called, it can be called again
|
||||
immediately to determine the next right-most digit since the score address
|
||||
(0x01) is overwritten with the new score. 0x02 is never more than 4-bits, and no
|
||||
additional memory locations are needed. I believe this is the reason this
|
||||
algorithm was used over other algorithms that can convert from hexadecimal to
|
||||
decimal with a fixed number of steps.
|
||||
|
||||
## _Contra_'s Score Display Algorithm in Assembly from Source
|
||||
The code for converting the scores for display is in the last (8th) PRG ROM
|
||||
bank, bank 7. The logic begins at address $caf8 in CPU memory. _Contra_ will
|
||||
recursively determine the value of the "Right Digit" place, which is then
|
||||
displayed on the screen to the player.
|
||||
|
||||
CPU memory addresses used
|
||||
* 0x00 - low byte of the current score being calculated
|
||||
* 0x01 - high byte of the current score being calculated
|
||||
* 0x02 - the next decimal digit to display
|
||||
* 0x03 - hard-coded 10 in decimal
|
||||
|
||||
```
|
||||
calculate_score_digit:
|
||||
lda #$00 ; set the accumulator register A to zero (#$00)
|
||||
sta $02 ; zero out any previously calculated digit
|
||||
ldy #$10 ; set the left-shift loop counter back to #$10 (16 decimal)
|
||||
rol $00 ; shift the score low byte to the left by one bit
|
||||
; push the most significant bit (msb) to the carry flag
|
||||
rol $01 ; shift the score high byte to the left by one byte
|
||||
; push the msb to the carry flag
|
||||
; pull in carry flag to least significant bit (lsb)
|
||||
|
||||
shift_and_check_digit_carry:
|
||||
rol $02 ; shift score high byte to the left by one bit
|
||||
; if the msb of the score high byte was 1, then carry into lsb
|
||||
lda $02 ; load current digit into the accumulator register A
|
||||
cmp $03 ; compare #$0a (10 decimal) to the current digit
|
||||
|
||||
; branch if current digit is less than #$0a (10 decimal)
|
||||
; - this means no subtraction and carry is needed
|
||||
; if greater than #$0a (10 decimal), don't jump
|
||||
; - subtract 10 and carry
|
||||
bcc continue_shift_score
|
||||
sbc $03 ; the current digit is greater than 10, subtract 10
|
||||
; this also sets the carry flag, which will be moved to the
|
||||
; low byte of the score, which is the "Rest" of the number
|
||||
; this carry represents adding 10 to the "Rest"
|
||||
sta $02 ; store the difference (new current digit) back in $02
|
||||
|
||||
; $02 (current digit) is less than #$0a, or has just been subtracted
|
||||
; continue algorithm by shifting score left
|
||||
continue_shift_score:
|
||||
rol $00 ; if just set $02 to digit by subtraction, this will put 1
|
||||
; in $00's lsb, signifying adding 10 to "Rest"
|
||||
rol $01 ; if $00's msb is 1, then it'll carry to the lsb of $01
|
||||
dey ; Y goes from $10 to $00, once Y is $00, the algorithm is done
|
||||
bne shift_and_check_digit_carry
|
||||
rts
|
||||
```
|
||||
|
||||
### See Also
|
||||
Converting a hexadecimal number to decimal can be done in multiple ways. A
|
||||
common algorithm for doing this is
|
||||
[double dabble algorithm](https://en.wikipedia.org/wiki/Double_dabble). _Contra_
|
||||
does not use this method. This is probably because the double dabble algorithm
|
||||
requires more CPU memory space, whereas _Contra_'s algorithm uses the same,
|
||||
fixed 2 bytes of CPU memory (0x01 and 0x02) for all score calculation.
|
||||
Consequently, instead of a fixed number of operations like double dabble,
|
||||
_Contra_ uses recursion, which requires a variable number of CPU cycle at the
|
||||
benefit of fixed memory usage.
|
||||
|
||||
Another interesting and related concept is the
|
||||
[binary-coded decimal](https://en.wikipedia.org/wiki/Binary-coded_decimal).
|
|
@ -0,0 +1,178 @@
|
|||
# Overview
|
||||
|
||||
_Contra_ loads various level-specific configuration data from in bank 2. These
|
||||
are called "level headers". These headers are #$20 (32) bytes for each level
|
||||
and contain information about the level, for example whether the level is and
|
||||
indoor level, or an outdoor level, vertical level or horizontal level, etc.
|
||||
|
||||
In the code, the level headers are labeled `level_headers` and each level is
|
||||
labeled with an appropriate `level_x_header` label.
|
||||
|
||||
# Byte Offset 0 - Location Type
|
||||
|
||||
* Offset: 0
|
||||
* Memory Location: $40
|
||||
* Variable Name: `LEVEL_LOCATION_TYPE`
|
||||
|
||||
The first byte is the level's "location type". There are 3 location types
|
||||
|
||||
* Outdoor - #$00
|
||||
* Indoor/Base - #$01
|
||||
* Indoor Boss - #$80
|
||||
|
||||
Outdoor levels are typical platformer side-scrolling levels and scrolls
|
||||
horizontally or vertically. Indoor levels are also known as base levels and are
|
||||
the levels where Bill and Lance are facing away from the player in a pseudo 3D
|
||||
experience (Levels 2 and 4).
|
||||
|
||||
Although the value is never #$80 in the level headers table, for the boss fights
|
||||
on the indoor levels (2 and 4), the location type will be set to $80. It is
|
||||
similar to the indoor location type (#$01), except the player cannot go prone
|
||||
and shooting up looks like shooting forward (z-plane). When animating the
|
||||
players walking into the screen between 3D screens, the value is also set to
|
||||
#$80.
|
||||
|
||||
# Byte Offset 1 - Outdoor Scrolling Type
|
||||
|
||||
* Offset: 1
|
||||
* Memory Location: $41
|
||||
* Variable Name: `LEVEL_SCROLLING_TYPE`
|
||||
|
||||
The second byte is the level's scrolling type. It is only used in outdoor
|
||||
levels.
|
||||
|
||||
* Horizontal - #$00
|
||||
* Vertical - #$01
|
||||
|
||||
# Byte Offsets 2 and 3 - Screen Super-Tile Pointer
|
||||
|
||||
* Offset: 2 and 3
|
||||
* Memory Location: 2-byte address $42 and $43
|
||||
* Variable Name: `LEVEL_SCREEN_SUPERTILES_PTR`
|
||||
|
||||
The 3rd and 4th bytes make up the pointer that point to the data that specifies
|
||||
which super-tiles are on each screen. This is an address into Bank 2. The
|
||||
address is 2 bytes and stored in little-endian, i.e. the least significant byte
|
||||
first.
|
||||
|
||||
For example, on level 3, the screen super tile pointer points to
|
||||
`level_3_supertiles_screen_ptr_table` (ROM: $84ce, MEM: Bank 2 $84ce). This
|
||||
address contains a list of pointers for each "screen" of the level. A screen is
|
||||
a set of super-tiles for a level that is 256 pixels wide. Since level 3 is a
|
||||
vertical level, each screen is 64 super-tiles (8x8). Looking at the data
|
||||
specified at the address of a specific screen pointer, e.g.
|
||||
`level_3_supertiles_screen_01` (ROM: $8635, MEM: Bank 2 $8635), you will find an
|
||||
RLE-encoded set of bytes that specify the super-tile indexes used. After
|
||||
decompressing, each byte is the offset into the
|
||||
`level_3_supertile_data` (ROM: $cef8, MEM: Bank 3 $8ef8).
|
||||
|
||||
# Byte Offsets 4 and 5 - Super-Tile Data
|
||||
|
||||
* Offset: 4 and 5
|
||||
* Memory Location: 2-byte address $44 and $45
|
||||
* Variable Name: `LEVEL_SUPERTILE_DATA_PTR`
|
||||
|
||||
The 5th and 6th bytes of the level headers are a pointer to the level's
|
||||
super-tiles. Each entry in table at the specified address is #$10 bytes long
|
||||
and is a single super-tile. This data is not RLE-encoded. Each byte in each
|
||||
super-tile represent a pattern table tile. An example pointer is
|
||||
`level_3_supertile_data` (ROM: $cef8, MEM: Bank 3 $8ef8)
|
||||
|
||||
# Byte Offsets 6 and 7 - Super-Tile Palette Data
|
||||
|
||||
* Offset: 6 and 7
|
||||
* Memory Location: 2-byte address $46 and $47
|
||||
* Variable Name: `LEVEL_SUPERTILE_PALETTE_DATA`
|
||||
|
||||
Address to byte table that specifies the palette used for each super-tile in the
|
||||
level. Each byte describes the 4 palettes for a single super-tile. An example
|
||||
is `level_2_palette_data`
|
||||
|
||||
# Byte Offset 8 - Alternate Graphics Loading Location
|
||||
|
||||
* Offset: 8
|
||||
* Location: $48
|
||||
* Variable Name: `LEVEL_ALT_GRAPHICS_POS`
|
||||
|
||||
This byte specifies the alternate graphics loading location. This is the screen
|
||||
where the game has the ability to change out the pattern table tiles for a
|
||||
level.
|
||||
|
||||
# Byte Offsets 9, 10, and 11 - Tile Collision Limits
|
||||
|
||||
* Offset: 9, 10, and 11
|
||||
* Location: $49, $4a, $4b
|
||||
* Variable Name: `COLLISION_CODE_1_TILE_INDEX`, `COLLISION_CODE_0_TILE_INDEX`,
|
||||
and `COLLISION_CODE_2_TILE_INDEX` (in that order)
|
||||
|
||||
These three bytes are used to specify which pattern table tiles are which
|
||||
collision codes.
|
||||
|
||||
* Pattern table tiles below the value in $49 (excluding #$00) are considered
|
||||
Collision Code 1 (floor).
|
||||
* Pattern table tiles >= the value in $49, but less than the value in $4a are
|
||||
considered Collision Code 0 (empty)
|
||||
* Pattern table tiles >= $4a but less than this tile index are considered
|
||||
Collision Code 2 (water)
|
||||
|
||||
Pattern table tiles above the last entry, are considered Collision Code 3
|
||||
(Solid).
|
||||
|
||||
# Byte Offset 12, 13, 14, and 15 - Cycling Palette Colors
|
||||
|
||||
* Offset: 12, 13, 14, and 15
|
||||
* Location: $4c, $4d, $4e, and $4f
|
||||
* Variable Name: `LEVEL_PALETTE_CYCLE_INDEXES`
|
||||
|
||||
Each level natively supports having its 4th nametable (background) palette cycle
|
||||
through up to 4 different sets of colors, where each set of colors is
|
||||
represented by an index into the `game_palettes` table. This header section
|
||||
contains those specific indexes to cycle through.
|
||||
|
||||
For more details see `Graphics Documentation.md`. There is a section titled
|
||||
`Palette -> Cycling`.
|
||||
|
||||
# Byte Offset 16, 17, 18, and 19 - Nametable Palette Initial Colors
|
||||
|
||||
* Offset: 16, 17, 18, and 19
|
||||
* Location: $50, $51, $52, and $53
|
||||
* Variable Name: `LEVEL_PALETTE_INDEX`
|
||||
|
||||
Initial background tile palette colors for the level. Indexes into
|
||||
`game_palettes`.
|
||||
|
||||
# Byte Offset 20, 21, 22, and 23 - Nametable Palette Initial Colors
|
||||
|
||||
* Offset: 20, 21, 22, and 23
|
||||
* Location: $54, $55, $56, and $57
|
||||
* Variable Name: `LEVEL_PALETTE_INDEX`
|
||||
|
||||
Initial sprite tile palette colors for the level. Indexes into `game_palettes`.
|
||||
|
||||
# Byte Offset 24 - Scroll Stop Screen
|
||||
|
||||
* Offset: 24
|
||||
* Location: $58
|
||||
* Variable Name: `LEVEL_STOP_SCROLL`
|
||||
|
||||
The screen of the level to stop scrolling. The value in memory is set to #$ff
|
||||
when boss auto scroll starts
|
||||
|
||||
# Byte Offset 25 - Solid Background Collision
|
||||
|
||||
* Offset: 25
|
||||
* Location: $59
|
||||
* Variable Name: `LEVEL_SOLID_BG_COLLISION_CHECK`
|
||||
|
||||
Specifies whether to check for bullet and weapon item solid bg collisions.
|
||||
|
||||
1. When non-zero, specifies that the weapon item should check for solid bg
|
||||
collisions (see `weapon_item_check_bg_collision`)
|
||||
2. When negative (bit 8 set), specifies to let bullets for both players and
|
||||
enemies to check for bullet-solid background collisions. This is used on
|
||||
levels 6 - energy zone and 7 - hangar. (see
|
||||
`check_bullet_solid_bg_collision` and `enemy_bullet_routine_01`)
|
||||
|
||||
# Byte Offset 26 through 32 - Unused
|
||||
|
||||
The last 6 bytes in each level's level header are unused and set to #$00.
|
|
@ -0,0 +1,306 @@
|
|||
# Overview
|
||||
A song or sound can be composed of multiple instruments or channels. The NES
|
||||
has 5 channels: 2 pulse (square wave) channels, 1 triangle channel, 1 noise
|
||||
channel, and 1 delta modulation channel. _Contra_ maintains 6 slots of data in
|
||||
memory that are used in priority order to play sounds. Higher slots are played
|
||||
before the lower slots. Each slot is linked to an NES sound channel.
|
||||
|
||||
* #$00 = pulse 1 channel
|
||||
* #$01 = pulse 2 channel
|
||||
* #$02 = triangle channel
|
||||
* #$03 = noise and dmc channel
|
||||
* #$04 = pulse 1
|
||||
* #$05 = noise channel
|
||||
|
||||
For example, if a sound is loaded in slot #$00 and slot #$04 (both pulse 1
|
||||
channel), then the sound in slot #$04 will be played since it is higher. The
|
||||
code that converts from sound slot to channel is `@load_sound_channel_offset`.
|
||||
|
||||
When the game loads a sound to play, it first determines which slots are needed
|
||||
by loading data from `sound_table_00`. This table specifies the number of slots
|
||||
needed to play the sound and where the instructions to play the sound exists,
|
||||
i.e. the 2 byte cpu address where the sound channel instructions are.
|
||||
|
||||
# Sound Codes
|
||||
|
||||
Below is a table of all the sounds that exist in _Contra_, including unused
|
||||
sounds. The Japanese names were obtained from the "sound mode" feature in the
|
||||
Famicom version of the game. Each sound is related to one or more sound codes.
|
||||
For example, the level 3 waterfall music uses 4 sound codes, which means that
|
||||
sound uses 4 sound channels.
|
||||
|
||||
At the bottom of the table are the DPCM samples that are used throughout the
|
||||
game by various other sounds.
|
||||
|
||||
| Sound | Japanese Name | Description | sound_code(s) | Slot | Command Type | Channel |
|
||||
|-------|---------------|------------------------------------------------------|---------------|------|--------------|------------------|
|
||||
| #$01 | | empty/silence, used to initialize channel | `sound_01` | | | |
|
||||
| #$02 | | percussive tick (bass drum/tom drum) | `sound_02` | 5 | low | noise |
|
||||
| #$03 | FOOT | player landing on ground or water | `sound_03` | 4 | low | pulse 1 |
|
||||
| | | | `sound_04` | 5 | low | noise |
|
||||
| #$05 | ROCK | waterfall rock landing on ground | `sound_05` | 4 | low | pulse 1 |
|
||||
| #$06 | TYPE 1 | unused, keyboard typing in Japanese version of game | `sound_06` | 4 | low | pulse 1 |
|
||||
| | | | `sound_07` | 5 | low | noise |
|
||||
| #$08 | | unused, rumbling | `sound_08` | 5 | low | noise |
|
||||
| #$09 | FIRE | energy zone fire beam | `sound_09` | 5 | low | noise |
|
||||
| #$0a | SHOTGUN1 | default weapon | `sound_0a` | 4 | low | pulse 1 |
|
||||
| | | | `sound_0b` | 5 | low | noise |
|
||||
| #$0c | SHOTGUN2 | M weapon, turret man | `sound_0c` | 4 | low | pulse 1 |
|
||||
| | | | `sound_0d` | 5 | low | noise |
|
||||
| #$0e | LASER | L weapon | `sound_0e` | 4 | low | pulse 1 |
|
||||
| | | | `sound_0f` | 5 | low | noise |
|
||||
| #$10 | PL FIRE | F weapon | `sound_10` | 4 | low | pulse 1 |
|
||||
| | | | `sound_11` | 5 | low | noise |
|
||||
| #$12 | SPREAD | S weapon | `sound_12` | 4 | low | pulse 1 |
|
||||
| | | | `sound_13` | 5 | low | noise |
|
||||
| #$14 | HIBIWARE | bullet shielded wall plating ting | `sound_14` | 4 | low | pulse 1 |
|
||||
| #$15 | CHAKUCHI | energy zone boss landing | `sound_15` | 4 | low | pulse 1 |
|
||||
| #$16 | DAMEGE 1 | bullet to metal collision ting | `sound_16` | 4 | low | pulse 1 |
|
||||
| | | | `sound_17` | 5 | low | noise |
|
||||
| #$18 | DAMEGE 2 | alien heart boss hit | `sound_18` | 4 | low | pulse 1 |
|
||||
| #$19 | TEKI OUT | enemy destroyed | `sound_19` | 4 | low | pulse 1 |
|
||||
| #$1a | HIRAI 1 | ice grenade whistling noise | `sound_1a` | 4 | low | pulse 1 |
|
||||
| #$1b | SENSOR | level 1 jungle boss siren | `sound_1b` | 4 | low | pulse 1 |
|
||||
| #$1c | KANDEN | electrocution sound | `sound_1c` | 4 | low | pulse 1 |
|
||||
| | | | `sound_1d` | 5 | low | noise |
|
||||
| #$1e | CAR | tank advancing | `sound_1e` | 4 | low | noise |
|
||||
| #$1f | POWER UP | pick up weapon item | `sound_1f` | 4 | low | pulse 1 |
|
||||
| #$20 | 1UP | extra life | `sound_20` | 4 | low | pulse 1 |
|
||||
| #$21 | HERI | helicopter rotors | `sound_21` | 4 | low | pulse 1 |
|
||||
| | | | `sound_21` | 1 | low | pulse 2 |
|
||||
| | | | `sound_23` | 5 | low | noise |
|
||||
| #$24 | BAKUHA 1 | explosion | `sound_24` | 5 | low | noise |
|
||||
| #$25 | BAKUHA 2 | game intro, indoor wall, and island explosion | `sound_25` | 5 | low | noise |
|
||||
| #$26 | TITLE | game intro tune | `sound_26` | 0 | high | pulse 1 |
|
||||
| | | | `sound_27` | 1 | high | pulse 2 |
|
||||
| | | | `sound_28` | 2 | high | triangle |
|
||||
| | | | `sound_29` | 3 | high | noise |
|
||||
| #$2a | BGM 1 | level 1 jungle and level 7 hangar music | `sound_2a` | 0 | high | pulse 1 |
|
||||
| | | | `sound_2b` | 1 | high | pulse 2 |
|
||||
| | | | `sound_2c` | 2 | high | triangle |
|
||||
| | | | `sound_2d` | 3 | high | noise |
|
||||
| #$2e | BGM 2 | level 3 waterfall music | `sound_2e` | 0 | high | pulse 1 |
|
||||
| | | | `sound_2f` | 1 | high | pulse 2 |
|
||||
| | | | `sound_30` | 2 | high | triangle |
|
||||
| | | | `sound_31` | 3 | high | noise |
|
||||
| #$32 | BGM 3 | level 5 snow field music | `sound_32` | 0 | high | pulse 1 |
|
||||
| | | | `sound_33` | 1 | high | pulse 2 |
|
||||
| | | | `sound_34` | 2 | high | triangle |
|
||||
| | | | `sound_35` | 3 | high | noise |
|
||||
| #$36 | BGM 4 | level 6 energy zone | `sound_36` | 0 | high | pulse 1 |
|
||||
| | | | `sound_37` | 1 | high | pulse 2 |
|
||||
| | | | `sound_38` | 2 | high | triangle |
|
||||
| | | | `sound_39` | 3 | high | noise |
|
||||
| #$3a | BGM 5 | level 8 alien's lair music | `sound_3a` | 0 | high | pulse 1 |
|
||||
| | | | `sound_3b` | 1 | high | pulse 2 |
|
||||
| | | | `sound_3c` | 2 | high | triangle |
|
||||
| | | | `sound_3d` | 3 | high | noise |
|
||||
| #$3e | 3D BGM | indoor/base level music | `sound_3e` | 0 | high | pulse 1 |
|
||||
| | | | `sound_3f` | 1 | high | pulse 2 |
|
||||
| | | | `sound_40` | 2 | high | triangle |
|
||||
| | | | `sound_41` | 3 | high | noise |
|
||||
| #$42 | BOSS | indoor/base boss screen music | `sound_42` | 0 | high | pulse 1 |
|
||||
| | | | `sound_43` | 1 | high | pulse 2 |
|
||||
| | | | `sound_44` | 2 | high | triangle |
|
||||
| | | | `sound_45` | 3 | high | noise |
|
||||
| #$46 | PCLR | end of level tune | `sound_46` | 0 | high | pulse 1 |
|
||||
| | | | `sound_47` | 1 | high | pulse 2 |
|
||||
| | | | `sound_48` | 2 | high | triangle |
|
||||
| | | | `sound_49` | 3 | high | noise |
|
||||
| #$4a | ENDING | end credits | `sound_4a` | 0 | high | pulse 1 |
|
||||
| | | | `sound_4b` | 1 | high | pulse 2 |
|
||||
| | | | `sound_4c` | 2 | high | triangle |
|
||||
| | | | `sound_4d` | 3 | high | noise |
|
||||
| #$4e | OVER | game over/after end credits, presented by Konami | `sound_4e` | 0 | high | pulse 1 |
|
||||
| | | | `sound_4f` | 1 | high | pulse 2 |
|
||||
| | | | `sound_50` | 2 | high | triangle |
|
||||
| | | | `sound_51` | 3 | high | noise |
|
||||
| #$52 | PL OUT | player death | `sound_52` | 4 | low | pulse 1 |
|
||||
| | | | `sound_53` | 5 | low | noise |
|
||||
| #$54 | | game pausing | `sound_54` | 4 | low | pulse 1 |
|
||||
| #$55 | BOSS BK | tank, boss ufo, boss giant, alien guardian destroyed | `sound_55` | 4 | low | pulse 1 |
|
||||
| | | | `sound_56` | 5 | low | noise |
|
||||
| #$57 | BOSS OUT | boss destroyed | `sound_57` | 4 | low | pulse 1 |
|
||||
| | | | `sound_58` | 1 | low | pulse 2 |
|
||||
| | | | `sound_59` | 5 | low | noise |
|
||||
| #$5a | n/a | high hat | n/a | n/a | low | delta modulation |
|
||||
| #$5b | n/a | snare | n/a | n/a | low | delta modulation |
|
||||
| #$5c | n/a | high hat | n/a | n/a | low | delta modulation |
|
||||
| #$ff | n/a | snowfield boss defeated door open (bug) | n/a | n/a | low | delta modulation |
|
||||
|
||||
|
||||
The sound for pausing the game is not in the sound mode menu, presumably to not
|
||||
confuse players into thinking the game is paused. As for names, I can guess at
|
||||
some of the abbreviations and name meanings.
|
||||
|
||||
* BAKUHA - ばくはつ (爆発) - Japanese for explosion
|
||||
* BGM - background music
|
||||
* BK - BAKUHA, i.e. explosion
|
||||
* CHAKUCHI - ちゃくち (着地) - Japanese for landing/touching the ground
|
||||
* HIBIWARE - ひびわれ (罅割れ, ひび割れ) - Japanese for crack; crevice; fissure
|
||||
* PCLR - player clear, or pattern clear
|
||||
* PL - player
|
||||
* TYPE - keyboard typing
|
||||
|
||||
# sound_code Parsing
|
||||
|
||||
Every video frame, the game loops through each sound slot to see if a sound is
|
||||
currently playing, see `@sound_slot_loop`. If a sound slot is populated, i.e.
|
||||
a sound is playing, then `handle_sound_code` will be called on that slot.
|
||||
`handle_sound_code` will first check if the game is paused, as the music and
|
||||
sound effects are paused when the game is paused. If the game isn't paused, the
|
||||
`handle_sound_code` will decrement the current sound slot's sound length
|
||||
(`SOUND_CMD_LENGTH`) and if the sound is finished, move to read the next command
|
||||
(`read_sound_command_00`). If the sound isn't finished, then
|
||||
`@pulse_vol_and_vibrato` is called to possibly adjust the volume and frequency
|
||||
of the current playing sound. Note frequency adjustments, i.e. vibrato isn't
|
||||
used by _Contra_.
|
||||
|
||||
A `sound_code` is composed of 1 or more sound commands. Each sound command will
|
||||
configure variables or set APU registers. Not every sound command will make a
|
||||
sound. Some just configure variables for the subsequent sound code, for example
|
||||
setting `SOUND_LENGTH_MULTIPLIER` for use by a subsequent sound command. The
|
||||
sound commands are parsed according to the following logic.
|
||||
|
||||
The first byte of the entire sound code determines how the rest of the commands
|
||||
for the sound code will be parsed. When the byte is less than #$30, the the
|
||||
sound code is considered a 'low sound' code. Otherwise, the code will be parsed
|
||||
as a 'high sound' code.
|
||||
|
||||
## sound_code Sharing
|
||||
|
||||
All sound command types can reference addresses to other sound commands. When a
|
||||
sound command moves to another command, once that command is finished executing,
|
||||
then the sound read pointer goes back to the next bytes in the original command.
|
||||
|
||||
* #$fd - move to child sound command for playing shared sound data across
|
||||
different `sound_xx` commands, or shared parts within the same sound code.
|
||||
Move to execute sound command at address specified by next 2 bytes.
|
||||
* #$fe - repeat the next sound command at address `a` `n` times, where `a` is
|
||||
the next byte and `n` is the 2nd and 3rd byte.
|
||||
* #$ff - finished reading sound command, exit to previous command if child
|
||||
sound command, otherwise, finished entire sound code
|
||||
|
||||
## Low Sound Command
|
||||
|
||||
In _Contra_, low sound commands are used by sound slots #$01 (pulse 1), #$04
|
||||
(pulse 2) and #$05 (noise). Low sound commands are used for sound effects.
|
||||
The method in code for parsing low sound commands is `read_low_sound_cmd`. In
|
||||
general, low sound commands set the length, decrescendo start, pitch, and duty.
|
||||
|
||||
The first byte of the sound command dictates how the subsequent bytes are
|
||||
interpreted. The command is read recursively until the note period is set, then
|
||||
the parsing exits.
|
||||
|
||||
* **Case 1** - #$2x - sets the number of video frames to wait before reading
|
||||
the next sound command (`SOUND_LENGTH_MULTIPLIER`) as well as the high
|
||||
nibble of the APU configuration register for the sound channel
|
||||
(`SOUND_CFG_HIGH`).
|
||||
* when low nibble is not #$f, then `SOUND_LENGTH_MULTIPLIER` is set to low
|
||||
nibble.
|
||||
* when low nibble is #$f, then the next full byte is used to set
|
||||
`SOUND_LENGTH_MULTIPLIER`
|
||||
* the following byte is then used to set high nibble of the APU
|
||||
configuration register for the sound channel (`SOUND_CFG_HIGH`)
|
||||
* **Case 2** - #$10 - enable/disable sweep and set volume decrescendo. What
|
||||
is set is based on the next byte. Additionally, if the sound slot is #$04,
|
||||
then the pulse 1 channel `PULSE_VOL_DURATION` will also be set to the sweep
|
||||
value.
|
||||
* non-zero - sweep will be enabled and set to the value of the byte.
|
||||
* #$00 - if the byte after #$10 is #$00, then sweep will be disabled by
|
||||
setting the APU register $4001 to #$7f.
|
||||
be set to #$7f.
|
||||
* **Case 3** - #$1x - slightly flatten the note that will be played by less
|
||||
than 1Hz by setting bit 4 of `SOUND_FLAGS`. The low nibble is not used.
|
||||
This case is not used in _Contra_. However, notes are flattened in another
|
||||
flow, see `@flip_flatten_note_adv`.
|
||||
* **Case 4** - #$xx - if not #2x, or #$1x, then byte high nibble is used as
|
||||
the low nibble (volume) for APU channel config. The high nibble and low
|
||||
nibble from memory are merged (`SOUND_CFG_HIGH` and `SOUND_CFG_LOW`
|
||||
respectively), unless volume is constant, then only the high nibble is used
|
||||
when setting the sound channel configuration ($4000). Also, the sound length
|
||||
(`SOUND_CMD_LENGTH`), value is set based on `SOUND_LENGTH_MULTIPLIER`.
|
||||
Finally, the note frequency/counter/pitch is set based off the next two
|
||||
bytes and then the parsing is complete.
|
||||
|
||||
After case 4 is parsed, the read low sound command method exits. Then the game
|
||||
logic will continue. The next frame, a standard `@sound_slot_loop` will pick up
|
||||
the sound slot is populated and play possibly modify the sound's volume and
|
||||
frequency for vibrato (not used in _Contra_). Every video frame, the game logic
|
||||
repeats this until the sound is completed, the game logic will then continue
|
||||
by reading the next byte of the next low sound code. This entire process
|
||||
repeats until a #$ff is read.
|
||||
|
||||
## High Sound Command
|
||||
|
||||
In _Contra_, high sound commands are used by sound slots #$00 (pulse 1), #$01
|
||||
(pulse 2), #$02 (triangle) and #$03 (noise & dmc channel). High sound commands
|
||||
are used for the 'music' of the game: the level background music, the intro
|
||||
tune, end credits song, and after credits/game over music. The method in code
|
||||
for parsing low sound commands is `read_high_sound_cmd`.
|
||||
|
||||
The first byte of the sound command dictates how the subsequent bytes are
|
||||
interpreted.
|
||||
|
||||
* **Case 1** - if the sound slot is #$03 (noise and dmc channel), then
|
||||
`parse_percussion_cmd` is called to handle the percussion. See notes below
|
||||
in section titled `Percussion Command`.
|
||||
* **Case 2** - if byte 0 high nibble is less than #$c, then `simple_sound_cmd`
|
||||
is used. This plays a single note with a specified length change from
|
||||
previous note with a volume envelope specified by `lvl_config_pulse`.
|
||||
* **Case 3** - if byte 0 high nibble is greater than or equal to #$0c, then
|
||||
`@regular_sound_cmd` is used. `@regular_sound_cmd` looks at bits 4 and 5 to
|
||||
know which entry in `sound_cmd_ptr_tbl` to use to handle the sound command.
|
||||
For details of what each method does, see section `sound_cmd_routine_xx`.
|
||||
|
||||
### sound_cmd_routine_xx
|
||||
* `sound_cmd_routine_00` - sets sound channel config to mute, marks channel
|
||||
as muted by setting bit 6 of `SOUND_FLAGS`.
|
||||
* `sound_cmd_routine_01` - sets sound length multiplier
|
||||
(`SOUND_LENGTH_MULTIPLIER`) to low nibble. Sets in memory low nibble of
|
||||
sound channel. Can initialize channel by calling
|
||||
`exe_channel_init_ptr_tbl_routine`. Otherwise, recursively calls back to
|
||||
`read_high_sound_cmd` to handle next byte.
|
||||
* `sound_cmd_routine_02`
|
||||
* If the low nibble is less than #$5, then sets note adjustment flag
|
||||
(`SOUND_PERIOD_ROTATE`) and recursively calls back to `read_high_sound_cmd`.
|
||||
* If the low nibble is #$8, set bit 4 of `SOUND_FLAGS` to mark note as
|
||||
slightly flattened from original value.
|
||||
* If the low nibble is #$b, set vibrato variables and recursively call
|
||||
`read_high_sound_cmd`.
|
||||
* If the low nibble is #$c, set the pitch based on next sound byte, which
|
||||
(when doubled) is an offset into `note_period_tbl`.
|
||||
* If the low nibble isn't any known value, just ignore it and recursively
|
||||
call `read_high_sound_cmd`.
|
||||
* `sound_cmd_routine_03` - this function handles the end of a sound command
|
||||
and determines where to go next based on the byte value. See section
|
||||
above titled `sound_code Sharing`.
|
||||
|
||||
### Percussion Command
|
||||
|
||||
Percussion commands are recursively read until Case 1 or Case 3 is reached.
|
||||
|
||||
* **Case 1** - The byte's high nibble is #$f. This is an end of sound command,
|
||||
the sound command will either end, repeat, or move back to parent sound
|
||||
command. See the section titled `sound_code Sharing`.
|
||||
* **Case 2** - The byte's high nibble is #$d. The low nibble is used to set
|
||||
the sound length multiplier (`SOUND_LENGTH_MULTIPLIER`).
|
||||
* **Case 3** - The byte high nibble isn't #$f, nor #$d. In this case, call
|
||||
`calc_cmd_len_play_percussion` to determine sound command length
|
||||
(`SOUND_CMD_LENGTH`) based on the low nibble and the value determined from
|
||||
Case 2. Then call `play_percussive_sound`. This method will use the high
|
||||
nibble (shifted into low nibble) of the byte value to get offset into
|
||||
`percussion_tbl`, which specifies which DMC sound sample code to play. This
|
||||
value is passed to `play_sound` to play the sound code. Then, if the value
|
||||
was greater than or equal to #$3, `sound_02` is also played with other sound
|
||||
code. The offsets are defined and which sound(s) is/are played are below.
|
||||
Note that in offset 5, `sound_02` is not played because there is a check in
|
||||
`load_sound_code_entry` and `sound_25` is already playing in slot #$05.
|
||||
* 0 - `sound_02`
|
||||
* 1 - `sound_5a`
|
||||
* 2 - `sound_5b`
|
||||
* 3 - `sound_5a` and `sound_02`
|
||||
* 4 - `sound_5b` and `sound_02`
|
||||
* 5 - `sound_25`
|
||||
* 6 - `sound_5c` and `sound_02`
|
||||
* 7 - `sound_5d` and `sound_02`
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,15 @@
|
|||
%% This is a mermaid file (https://github.com/mermaid-js/mermaid)
|
||||
%% This shows the very high level of the game engine
|
||||
|
||||
graph TD
|
||||
nmi_start --> handle_sound_slots
|
||||
handle_sound_slots --> load_controller_state
|
||||
load_controller_state --> exe_game_routine
|
||||
exe_game_routine --> game_routine_01 --> draw_sprites
|
||||
exe_game_routine --> game_routine_02 --> draw_sprites
|
||||
exe_game_routine --> game_routine_03 --> draw_sprites
|
||||
exe_game_routine --> game_routine_04 --> draw_sprites
|
||||
exe_game_routine --> game_routine_05
|
||||
game_routine_05 --> run_level_routine --> draw_sprites
|
||||
exe_game_routine --> game_routine_06 --> draw_sprites
|
||||
draw_sprites --> remove_registers_from_stack_and_rti
|
|
@ -0,0 +1,42 @@
|
|||
%% This is a mermaid file (https://github.com/mermaid-js/mermaid)
|
||||
%% This shows the paths from which the graphics are updated
|
||||
flowchart TD
|
||||
level_routine_01:::entry
|
||||
level_routine_01 --> draw_stage_and_level_name
|
||||
draw_stage_and_level_name --> load_bank_6_write_text_palette_to_mem
|
||||
level_routine_01 --> draw_player_num_lives
|
||||
draw_player_num_lives --> load_bank_6_write_text_palette_to_mem
|
||||
|
||||
level_routine_00:::entry
|
||||
level_routine_00 --> |load transition_screen_palettes|load_bank_6_write_text_palette_to_mem
|
||||
load_bank_6_write_text_palette_to_mem --> write_text_palette_to_mem
|
||||
level_routine_00 --> zero_out_nametables
|
||||
zero_out_nametables --> |graphic_data_0|write_graphic_data_to_ppu
|
||||
|
||||
level_routine_04:::entry
|
||||
level_routine_04 --> handle_scroll
|
||||
handle_scroll --> load_column_of_tiles_to_cpu_buffer
|
||||
load_column_of_tiles_to_cpu_buffer --> load_level_supertile_data
|
||||
|
||||
level_routine_05:::entry
|
||||
level_routine_05 --> load_level_intro
|
||||
load_level_intro --> load_A_offset_graphic_data
|
||||
load_A_offset_graphic_data --> load_level_graphic_data
|
||||
load_level_graphic_data --> write_graphic_data_to_ppu
|
||||
|
||||
level_routine_03:::entry
|
||||
level_routine_03 --> init_lvl_nametable_animation
|
||||
init_lvl_nametable_animation --> load_column_of_tiles_to_cpu_buffer
|
||||
init_lvl_nametable_animation --> write_col_attribute_to_cpu_memory
|
||||
|
||||
level_routine_02:::entry
|
||||
level_routine_02 --> zero_out_nametables
|
||||
level_routine_02 --> load_level_graphics
|
||||
load_level_graphics --> load_current_level_graphic_data
|
||||
load_current_level_graphic_data --> write_graphic_data_to_ppu
|
||||
level_routine_02 --> load_palettes_color_to_cpu
|
||||
|
||||
nmi_start:::entry
|
||||
nmi_start --> write_cpu_graphics_buffer_to_ppu
|
||||
|
||||
classDef entry fill:#f96
|
|
@ -0,0 +1,18 @@
|
|||
%% This is a mermaid file (https://github.com/mermaid-js/mermaid)
|
||||
%% This shows the ordering of the states among the level routines
|
||||
|
||||
graph TD
|
||||
level_routine_00 --> level_routine_01
|
||||
level_routine_01 --> level_routine_02
|
||||
level_routine_02 --> level_routine_03
|
||||
level_routine_03 --> level_routine_04
|
||||
level_routine_04 --> |Level Complete|level_routine_08
|
||||
level_routine_04 --> |Game Over|level_routine_0a
|
||||
level_routine_05 --> |Level Complete|level_routine_00
|
||||
level_routine_05 --> |Complete Last Level/Game Over|level_routine_06
|
||||
level_routine_05 --> |Game Over No Continues|level_routine_07
|
||||
level_routine_06 --> |Continue/End Selected|level_routine_00
|
||||
level_routine_08 --> |Game Over|level_routine_0a
|
||||
level_routine_08 --> |Level Complete|level_routine_09
|
||||
level_routine_0a --> |Game Over Timer Expired|level_routine_05
|
||||
level_routine_09 --> |Level Complete|level_routine_05
|
|
@ -0,0 +1,25 @@
|
|||
%% This is a mermaid file (https://github.com/mermaid-js/mermaid)
|
||||
%% This shows the simplified overview of the audio engine
|
||||
flowchart TD
|
||||
|
||||
6(read_sound_command_00) -->|< #$30|8(read_low_sound_cmd)
|
||||
6 --> |>= #$30|9(read_high_sound_cmd)
|
||||
9 --> |sound slot 3|11(parse_percussion_cmd)
|
||||
11 --> 16
|
||||
9 --> 13("@regular_sound_cmd")
|
||||
9 --> 14(simple_sound_cmd)
|
||||
14 --> |set note|42[rts]
|
||||
8 --> 16(sound_cmd_routine_03)
|
||||
16 --> 43[rts]
|
||||
16 -.-> |next sound command|6
|
||||
8 --> 17(interpret_sound_byte)
|
||||
17 --> |set note|46[rts]
|
||||
17 -.-> 8
|
||||
13 --> 39(sound_cmd_routine_00)
|
||||
39 --> 44[rts]
|
||||
13 --> 40(sound_cmd_routine_01)
|
||||
13 --> 41(sound_cmd_routine_02)
|
||||
13 --> 16
|
||||
41 -.-> 9
|
||||
40 --> 45[rts]
|
||||
40 -.-> 9
|
|
@ -0,0 +1,49 @@
|
|||
%% This is a mermaid file (https://github.com/mermaid-js/mermaid)
|
||||
%% This shows in detail the logic flow for the audio engine
|
||||
|
||||
flowchart TD
|
||||
|
||||
5["@check_sound_command"] --> |read next command|6(read_sound_command_00)
|
||||
6 -->|command byte < #$30|8(read_low_sound_cmd)
|
||||
6 -->|command byte >= #$30|9(read_high_sound_cmd)
|
||||
9 --> |noise channel|11(parse_percussion_cmd)
|
||||
9 --> |"not noise regular command (>=#$c0)"|13("@regular_sound_cmd")
|
||||
9 --> |"not noise simple command (< #$c0)"|14(simple_sound_cmd)
|
||||
8 --> |not control byte|17(interpret_sound_byte)
|
||||
8 --> |control byte\n#$fd, #$fe, #$ff|16(sound_cmd_routine_03)
|
||||
11 --> |high nibble #$f|16
|
||||
11 --> |high nibble #$d|20(control_nibble_d)
|
||||
11 --> |high nibble not #$d not #$f|21(sound_byte_calc_cmd_delay)
|
||||
17 --> |high nibble not #$2|24("@high_nibble_not_2")
|
||||
17 --> |high nibble #$2|52[set length multiplier and config]
|
||||
52 -.-> 8
|
||||
24 --> |high nibble #$1 low nibble not #$0|54[flatten note]
|
||||
54 -.-> 8
|
||||
24 --> |high nibble #$1 low nibble #$0|26[set sweep if specified]
|
||||
24 --> |high nibble not #$1|27("@high_nibble_not_1")
|
||||
26 -.-> 8
|
||||
27 --> 55[set sound multiplier]
|
||||
55 --> 56(set_note)
|
||||
56 -.-> 8
|
||||
20 --> 28["SOUND_LENGTH_MULTIPLIER = low nibble"]
|
||||
28 -.-> |read next byte|11
|
||||
16 --> |low nibble < #$0e|33[set NEW_SOUND_CODE_LOW_ADDR, and sound flag bit 3]
|
||||
16 --> |low nibble #$f|32(low_nibble_f)
|
||||
16 --> |low nibble #$e|38(read_sound_command_01)
|
||||
33 --> 34(load_sound_code_addr)
|
||||
32 --> |SOUND_FLAGS bit 3 set|36(move_new_sound_code)
|
||||
32 --> |SOUND_FLAGS bit 3 clear|37(exe_channel_init_ptr_tbl_routine)
|
||||
38 -.-> 6
|
||||
13 --> 16
|
||||
13 --> 39(sound_cmd_routine_00)
|
||||
13 --> 40(sound_cmd_routine_01)
|
||||
13 --> 41(sound_cmd_routine_02)
|
||||
41 --> |low nibble >= #$05|43("@high_val")
|
||||
41 --> |low nibble < #$05|44(set SOUND_PERIOD_ROTATE)
|
||||
44 -.-> |advance sound byte|9
|
||||
43 --> |low nibble #$08|46("@flip_flatten_note_adv")
|
||||
43 --> |low nibble #$0b|48("@set_vibrato_vars_adv")
|
||||
43 --> |low nibble #$0c|50("@set_pitch_adj_adv")
|
||||
43 -.-> |unsupported value, advance sound byte|9
|
||||
50 --> |advance sound byte|51(set SOUND_PITCH_ADJ)
|
||||
51 -.-> |advance sound byte|9
|
|
@ -0,0 +1,9 @@
|
|||
while (true) do
|
||||
memory.writebyte(0x00b0, 0xff) -- player 1
|
||||
memory.writebyte(0x00b1, 0xff) -- player 2
|
||||
|
||||
-- 0xb0/b1 in memory store the amount of time the B weapon (barrier) lasts.
|
||||
-- By continually overwriting the timer to the max value (FF), the players
|
||||
-- will always be invincible
|
||||
emu.frameadvance();
|
||||
end;
|
|
@ -0,0 +1,24 @@
|
|||
while (true) do
|
||||
local p1score1 = memory.readbyte(0x07e2)
|
||||
local p1score2 = memory.readbyte(0x07e3)
|
||||
p1score = (bit.lshift(p1score2, 8) + p1score1) * 100
|
||||
|
||||
local weapon_hex = memory.readbyte(0x00aa)
|
||||
local rapid_fire = ""
|
||||
if bit.band(weapon_hex, 0x10) == 0x10 then
|
||||
rapid_fire = " Rapid "
|
||||
else
|
||||
rapid_fire = " "
|
||||
end
|
||||
|
||||
weapon_hex = bit.band(weapon_hex, 0x0f)
|
||||
local weapon_name = ""
|
||||
|
||||
if weapon_hex == 0 then weapon_name = "Default Gun"
|
||||
elseif weapon_hex == 1 then weapon_name = "Machine Gun"
|
||||
elseif weapon_hex == 2 then weapon_name = "Flame Thrower"
|
||||
elseif weapon_hex == 3 then weapon_name = "Spray"
|
||||
elseif weapon_hex == 4 then weapon_name = "Laser" end
|
||||
gui.text(5, 40, "P1: " .. p1score .. rapid_fire .. weapon_name);
|
||||
emu.frameadvance();
|
||||
end;
|
|
@ -0,0 +1,9 @@
|
|||
while (true) do
|
||||
memory.writebyte(0x00b0, 0xff) -- player 1
|
||||
memory.writebyte(0x00b1, 0xff) -- player 2
|
||||
|
||||
-- 0xb0/b1 in memory store the amount of time the B weapon (barrier) lasts.
|
||||
-- By continually overwriting the timer to the max value (FF), the players
|
||||
-- will always be invincible
|
||||
emu.frameadvance();
|
||||
end;
|
|
@ -0,0 +1,37 @@
|
|||
-- used for testing to show enemy data
|
||||
|
||||
function display_enemy_data()
|
||||
for i = 0,0xf do
|
||||
local ENEMY_X_POS = emu.read(0x033e + i, emu.memType.cpu)
|
||||
local ENEMY_Y_POS = emu.read(0x0324 + i, emu.memType.cpu)
|
||||
local ENEMY_ANIMATION_DELAY = emu.read(0x0538 + i, emu.memType.cpu)
|
||||
local ENEMY_ATTRIBUTES = emu.read(0x05a8 + i, emu.memType.cpu)
|
||||
local ENEMY_FRAME = emu.read(0x0568 + i, emu.memType.cpu)
|
||||
local ENEMY_HP = emu.read(0x0578 + i, emu.memType.cpu)
|
||||
local ENEMY_STATE_WIDTH = emu.read(0x0598 + i, emu.memType.cpu)
|
||||
local ENEMY_VAR_A = emu.read(0x0548 + i, emu.memType.cpu)
|
||||
local ENEMY_VAR_B = emu.read(0x0558 + i, emu.memType.cpu)
|
||||
local ENEMY_VAR_1 = emu.read(0x05b8 + i, emu.memType.cpu)
|
||||
local ENEMY_VAR_2 = emu.read(0x05c8 + i, emu.memType.cpu)
|
||||
local ENEMY_VAR_3 = emu.read(0x05d8 + i, emu.memType.cpu)
|
||||
local ENEMY_VAR_4 = emu.read(0x05e8 + i, emu.memType.cpu)
|
||||
local ENEMY_SPRITES = emu.read(0x030a + i, emu.memType.cpu)
|
||||
local ENEMY_ROUTINE = emu.read(0x04b8 + i, emu.memType.cpu)
|
||||
local ENEMY_ATTACK_DELAY = ENEMY_VAR_B
|
||||
local ENEMY_X_VELOCITY_FAST = emu.read(0x0508 + i, emu.memType.cpu)
|
||||
local ENEMY_X_VELOCITY_FRACT = emu.read(0x0518 + i, emu.memType.cpu)
|
||||
local ENEMY_X_VEL_ACCUM = emu.read(0x04d8 + i, emu.memType.cpu)
|
||||
local ENEMY_Y_VELOCITY_FAST = emu.read(0x04e8 + i, emu.memType.cpu)
|
||||
local ENEMY_Y_VELOCITY_FRACT = emu.read(0x04f8 + i, emu.memType.cpu)
|
||||
local ENEMY_Y_VEL_ACCUM = emu.read(0x04c8 + i, emu.memType.cpu)
|
||||
|
||||
-- change variable to interested variable for studying
|
||||
emu.drawString(ENEMY_X_POS, ENEMY_Y_POS, string.format("%x", ENEMY_HP))
|
||||
end
|
||||
end
|
||||
|
||||
function Main()
|
||||
display_enemy_data()
|
||||
end
|
||||
|
||||
emu.addEventCallback(Main, emu.eventType.endFrame)
|
|
@ -0,0 +1,72 @@
|
|||
-- shows the background collision codes
|
||||
-- floor, water, solid, or empty
|
||||
|
||||
function get_bg_collision(x, y)
|
||||
local VERTICAL_SCROLL = emu.read(0xfc, emu.memType.cpu)
|
||||
local HORIZONTAL_SCROLL = emu.read(0xfd, emu.memType.cpu)
|
||||
local PPUCTRL_SETTINGS = emu.read(0xff, emu.memType.cpu)
|
||||
local adjusted_y = y + VERTICAL_SCROLL
|
||||
local adjusted_x = x + HORIZONTAL_SCROLL
|
||||
|
||||
if adjusted_y >= 0xf0 then
|
||||
adjusted_y = adjusted_y + 0x0f
|
||||
adjusted_y = adjusted_y - 255
|
||||
end
|
||||
|
||||
-- $10 is always #$00, except when moving cart is calling get_bg_collision
|
||||
local nametable_number = (PPUCTRL_SETTINGS ~ 0x00) & 0x01
|
||||
if adjusted_x > 255 then
|
||||
nametable_number = nametable_number ~ 1
|
||||
adjusted_x = adjusted_x - 255
|
||||
end
|
||||
|
||||
adjusted_y = (adjusted_y >> 2) & 0x3c
|
||||
adjusted_x = adjusted_x >> 4
|
||||
local bg_collision_offset = (adjusted_x >> 2) | adjusted_y
|
||||
if nametable_number == 1 then
|
||||
bg_collision_offset = bg_collision_offset | 0x40;
|
||||
end
|
||||
|
||||
local collisionCodeByte = emu.read(0x680 + bg_collision_offset, emu.memType.cpu)
|
||||
adjusted_x = adjusted_x & 0x03;
|
||||
local collisionCode = 0
|
||||
if adjusted_x == 0 then
|
||||
collisionCode = collisionCodeByte >> 6
|
||||
elseif adjusted_x == 1 then
|
||||
collisionCode = collisionCodeByte >> 4
|
||||
elseif adjusted_x == 2 then
|
||||
collisionCode = collisionCodeByte >> 2
|
||||
else
|
||||
collisionCode = collisionCodeByte
|
||||
end
|
||||
|
||||
collisionCode = collisionCode & 0x03;
|
||||
|
||||
local floorColor = 0x508fbc8f
|
||||
local waterColor = 0x500096FF
|
||||
local solidColor = 0x50A9A9A9
|
||||
local tileColor = 0x0
|
||||
if collisionCode == 0x01 then
|
||||
tileColor = floorColor
|
||||
elseif collisionCode == 0x02 then
|
||||
tileColor = waterColor
|
||||
elseif collisionCode == 0x03 then
|
||||
tileColor = solidColor
|
||||
end
|
||||
|
||||
if collisionCode ~= 0 then
|
||||
emu.drawRectangle(x, y, 16, 16, tileColor, true)
|
||||
end
|
||||
end
|
||||
|
||||
function Main()
|
||||
local VERTICAL_SCROLL = emu.read(0xfc, emu.memType.cpu)
|
||||
local HORIZONTAL_SCROLL = emu.read(0xfd, emu.memType.cpu)
|
||||
for i = 0,300,16 do
|
||||
for j = 0,300,16 do
|
||||
get_bg_collision(i - math.fmod(HORIZONTAL_SCROLL, 16), j - math.fmod(VERTICAL_SCROLL, 16))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
emu.addEventCallback(Main, emu.eventType.endFrame)
|
|
@ -0,0 +1,106 @@
|
|||
-- shows the player - enemy collision boxes
|
||||
-- does not show the player bullet - enemy collision boxes
|
||||
-- doesn't currently handle collision code f (rising spiked walls and fire beams)
|
||||
|
||||
function check_player_x_collision(player_index)
|
||||
local PLAYER_STATE = emu.read(0x90 + player_index, emu.memType.cpu)
|
||||
if PLAYER_STATE ~= 0x01 then
|
||||
-- exit if player not in normal state
|
||||
return nil
|
||||
end
|
||||
|
||||
-- @check_in_water_crouched
|
||||
local collision_box = 0x00
|
||||
local LEVEL_LOCATION_TYPE = emu.read(0x40 + player_index, emu.memType.cpu)
|
||||
local PLAYER_WATER_STATE = emu.read(0xb2 + player_index, emu.memType.cpu)
|
||||
if LEVEL_LOCATION_TYPE & 0xfe == 0x00 then
|
||||
-- outdoor
|
||||
local PLAYER_SPRITE_SEQUENCE = emu.read(0xbc + player_index, emu.memType.cpu)
|
||||
if PLAYER_WATER_STATE ~= 0x00 and PLAYER_SPRITE_SEQUENCE == 0x02 then
|
||||
-- exit if player crouched in water, no collision can happen with player
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if PLAYER_WATER_STATE == 0x00 then
|
||||
collision_box = collision_box + 1
|
||||
end
|
||||
|
||||
-- not checking for crouched while on indoor level, because non-bullets can
|
||||
-- collide when crouching player on indoor levels
|
||||
|
||||
local PLAYER_JUMP_STATUS = emu.read(0xa0 + player_index, emu.memType.cpu)
|
||||
local PLAYER_SPRITE_CODE = emu.read(0xd6 + player_index, emu.memType.cpu)
|
||||
if PLAYER_JUMP_STATUS == 0x00 then
|
||||
-- player not jumping
|
||||
collision_box = collision_box + 1
|
||||
if PLAYER_SPRITE_CODE ~= 0x17 then
|
||||
collision_box = collision_box + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- draw player collision point
|
||||
local SPRITE_Y_POS = emu.read(0x031a + player_index, emu.memType.cpu)
|
||||
local SPRITE_X_POS = emu.read(0x0334 + player_index, emu.memType.cpu)
|
||||
emu.drawRectangle(SPRITE_X_POS, SPRITE_Y_POS, 1, 1, 0x0000ff, false)
|
||||
|
||||
for i = 0,0xf do
|
||||
local ENEMY_ROUTINE = emu.read(0x04b8 + i, emu.memType.cpu)
|
||||
local ENEMY_ROUTINE = emu.read(0x04b8 + i, emu.memType.cpu)
|
||||
local ENEMY_STATE_WIDTH = emu.read(0x0598 + i, emu.memType.cpu)
|
||||
local should_test_collision = ENEMY_STATE_WIDTH & 0x01 == 0x00
|
||||
if ENEMY_ROUTINE ~= 0x00 and should_test_collision then
|
||||
set_enemy_collision_box(player_index, i, collision_box)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function set_enemy_collision_box(player_index, i, collision_box)
|
||||
local ENEMY_SCORE_COLLISION = emu.read(0x0588 + i, emu.memType.cpu) & 0x0f
|
||||
|
||||
if ENEMY_SCORE_COLLISION == 0x0f then
|
||||
-- todo handle (fire beam and rising spiked wall)
|
||||
return
|
||||
end
|
||||
|
||||
local collision_box_addr = emu.readWord(0xe4e8 + (collision_box * 2), emu.memType.cpu) -- collision_box_codes_tbl
|
||||
local offset = ENEMY_SCORE_COLLISION * 4
|
||||
local y0 = emu.read(collision_box_addr + offset, emu.memType.cpu)
|
||||
local x0 = emu.read(collision_box_addr + offset + 1, emu.memType.cpu)
|
||||
local height = emu.read(collision_box_addr + offset + 2, emu.memType.cpu)
|
||||
local width = emu.read(collision_box_addr + offset + 3, emu.memType.cpu)
|
||||
local ENEMY_Y_POS = emu.read(0x0324 + i, emu.memType.cpu)
|
||||
local ENEMY_X_POS = emu.read(0x033e + i, emu.memType.cpu)
|
||||
|
||||
local topY = ENEMY_Y_POS
|
||||
if y0 > 0x80 then
|
||||
y0 = negateInt(y0)
|
||||
topY = ENEMY_Y_POS - y0
|
||||
else
|
||||
topY = ENEMY_Y_POS + y0
|
||||
end
|
||||
|
||||
local topX = ENEMY_X_POS
|
||||
if x0 > 0x80 then
|
||||
x0 = negateInt(x0)
|
||||
topX = ENEMY_X_POS - x0
|
||||
else
|
||||
topX = ENEMY_X_POS + x0
|
||||
end
|
||||
|
||||
emu.drawRectangle(topX, topY, width, height, 0x0000ff, false)
|
||||
end
|
||||
|
||||
function negateInt(num)
|
||||
if(num > 0x80) then
|
||||
num = (~num + 1) & 0xff
|
||||
end
|
||||
|
||||
return num
|
||||
end
|
||||
|
||||
function Main()
|
||||
check_player_x_collision(0)
|
||||
end
|
||||
|
||||
emu.addEventCallback(Main, emu.eventType.endFrame)
|
|
@ -0,0 +1,27 @@
|
|||
-- shows information about weapon on head-up-display area
|
||||
|
||||
function Main()
|
||||
local p1score1 = emu.read(0x07e2, emu.memType.cpu)
|
||||
local p1score2 = emu.read(0x07e3, emu.memType.cpu)
|
||||
p1score = ((p1score2 << 8) + p1score1) * 100
|
||||
|
||||
local weapon_hex = emu.read(0x00aa, emu.memType.cpu)
|
||||
local rapid_fire = ""
|
||||
if weapon_hex & 0x10 == 0x10 then
|
||||
rapid_fire = " Rapid "
|
||||
else
|
||||
rapid_fire = " "
|
||||
end
|
||||
|
||||
weapon_hex = weapon_hex & 0x0f
|
||||
local weapon_name = ""
|
||||
|
||||
if weapon_hex == 0 then weapon_name = "Default Gun"
|
||||
elseif weapon_hex == 1 then weapon_name = "Machine Gun"
|
||||
elseif weapon_hex == 2 then weapon_name = "Flame Thrower"
|
||||
elseif weapon_hex == 3 then weapon_name = "Spray"
|
||||
elseif weapon_hex == 4 then weapon_name = "Laser" end
|
||||
emu.drawString(5,40, "P1: " .. p1score .. rapid_fire .. weapon_name)
|
||||
end
|
||||
|
||||
emu.addEventCallback(Main, emu.eventType.endFrame)
|
|
@ -0,0 +1,45 @@
|
|||
This folder contains every sprite that can be loaded in Contra. The sprites
|
||||
here are not used by the game code, they were extracted from the ROM for ease of
|
||||
viewing.
|
||||
|
||||
The name of each sprite is the same as the label in bank1.asm in the 2 large
|
||||
sprite tables sprite_ptr_tbl_0 and sprite_ptr_tbl_1.
|
||||
|
||||
The player lives medals in the heads up display (HUD) are not in the sprite
|
||||
tables, but the tiles are written directly to the OAMDMA via the CPU buffer.
|
||||
|
||||
# Sprite Table
|
||||
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f |
|
||||
|---|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
|
||||
| 0 | | |   |   |   |   |   |  |   |   |   |   |   |   |   |   |
|
||||
| 1 |   |   |   |   |   |   |   |   |  |  |  |  |  |  |  |  |
|
||||
| 2 |  |  |  |  |  |  |  |  |  |  |  |  |  |  | |  |
|
||||
| 3 |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
|
||||
| 4 |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | |
|
||||
| 5 |   |   |   |   |   |   |   |   |   | | | | |  |  |  |
|
||||
| 6 |  |  |  |  |  | | | |  |  |  |  |  |  |  |  |
|
||||
| 7 |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | |
|
||||
| 8 | | |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
|
||||
| 9 |  |   |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
|
||||
| a |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
|
||||
| b |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
|
||||
| c |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
|
||||
|
||||
# Hud Sprites
|
||||
|
||||
* Player 1
|
||||
* 
|
||||
* 
|
||||
* Player 2
|
||||
* 
|
||||
* 
|
||||
|
||||
# Sprites Not Used In Game
|
||||
|
||||
*  (`sprite_78`) exists in the
|
||||
game code, but isn't used. It is identical to `sprite_74` and that is what is
|
||||
used insead.
|
||||
* `sprite_59`, `sprite_5a`, `sprite_5b`, `sprite_5c`, `sprite_65`, `sprite_66`,
|
||||
and `sprite_67` are all empty and not used in the game.
|
||||
* `sprite_80` and `sprite_81` are defined in the game, but never used so I can't
|
||||
tell which pattern tiles they are supposed to utilize.
|
After Width: | Height: | Size: 211 B |
After Width: | Height: | Size: 136 B |
After Width: | Height: | Size: 211 B |
After Width: | Height: | Size: 136 B |
After Width: | Height: | Size: 272 B |
After Width: | Height: | Size: 263 B |
After Width: | Height: | Size: 251 B |
After Width: | Height: | Size: 237 B |
After Width: | Height: | Size: 243 B |
After Width: | Height: | Size: 235 B |
After Width: | Height: | Size: 254 B |
After Width: | Height: | Size: 252 B |
After Width: | Height: | Size: 253 B |
After Width: | Height: | Size: 245 B |
After Width: | Height: | Size: 89 B |
After Width: | Height: | Size: 209 B |
After Width: | Height: | Size: 196 B |
After Width: | Height: | Size: 211 B |
After Width: | Height: | Size: 193 B |
After Width: | Height: | Size: 218 B |
After Width: | Height: | Size: 207 B |
After Width: | Height: | Size: 218 B |
After Width: | Height: | Size: 204 B |
After Width: | Height: | Size: 202 B |
After Width: | Height: | Size: 183 B |
After Width: | Height: | Size: 267 B |
After Width: | Height: | Size: 264 B |
After Width: | Height: | Size: 259 B |
After Width: | Height: | Size: 249 B |
After Width: | Height: | Size: 257 B |
After Width: | Height: | Size: 250 B |
After Width: | Height: | Size: 268 B |
After Width: | Height: | Size: 259 B |
After Width: | Height: | Size: 262 B |
After Width: | Height: | Size: 248 B |
After Width: | Height: | Size: 253 B |
After Width: | Height: | Size: 246 B |
After Width: | Height: | Size: 272 B |
After Width: | Height: | Size: 264 B |
After Width: | Height: | Size: 268 B |
After Width: | Height: | Size: 256 B |
After Width: | Height: | Size: 266 B |
After Width: | Height: | Size: 257 B |
After Width: | Height: | Size: 256 B |
After Width: | Height: | Size: 258 B |
After Width: | Height: | Size: 248 B |
After Width: | Height: | Size: 227 B |
After Width: | Height: | Size: 137 B |
After Width: | Height: | Size: 168 B |
After Width: | Height: | Size: 178 B |
After Width: | Height: | Size: 213 B |
After Width: | Height: | Size: 206 B |
After Width: | Height: | Size: 191 B |
After Width: | Height: | Size: 89 B |
After Width: | Height: | Size: 115 B |
After Width: | Height: | Size: 119 B |
After Width: | Height: | Size: 123 B |
After Width: | Height: | Size: 126 B |
After Width: | Height: | Size: 143 B |
After Width: | Height: | Size: 129 B |
After Width: | Height: | Size: 139 B |
After Width: | Height: | Size: 218 B |
After Width: | Height: | Size: 225 B |
After Width: | Height: | Size: 224 B |
After Width: | Height: | Size: 238 B |
After Width: | Height: | Size: 245 B |