├── Makefile ├── README.md ├── si78c.c └── si78c_proto.h /Makefile: -------------------------------------------------------------------------------- 1 | $(shell mkdir -p bin) 2 | 3 | CFLAGS := -I . -I /usr/include/SDL2 4 | UNAME := $(shell uname -s) 5 | 6 | ifeq ($(UNAME),Darwin) 7 | CFLAGS += -I /Library/Frameworks/SDL2.framework/Headers -F /Library/Frameworks/ -framework SDL2 8 | else 9 | LDFLAGS := -lSDL2 10 | endif 11 | 12 | all: bin/si78c 13 | 14 | clean: 15 | rm -rf bin/si78c 16 | 17 | bin/si78c: si78c.c si78c_proto.h 18 | gcc $(CFLAGS) $< -o $@ $(LDFLAGS) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # si78c 2 | 3 | `si78c` is a memory accurate reimplementation of the 1978 arcade game Space Invaders in C. 4 | 5 | It requires the original arcade ROM to function to load various sprites and 6 | other data, but does not use the original game code. 7 | 8 | It is not an emulation, but rather a restoration. 9 | 10 | It is however, accurate enough that it can be used to understand the inner 11 | workings of the original system, in a more accessible manner. 12 | 13 | Many thanks to Christopher Cantrell at computerarcheology.com. Without his 14 | painstaking work and excellent notes, this project would have taken a lot 15 | longer. 16 | 17 | # Project Scale 18 | 19 | This was a reasonably large undertaking, requiring many iterations over several 20 | months, and I would conservatively estimate that around 200 hours of work have 21 | been put into the project so far. 22 | 23 | The original ROM is around 2000 lines of 8080 assembler, all of it game code. 24 | The final published version of `si78c` is around 1500 lines of game code, 500 25 | lines of support code, and around 800 lines of comments. 26 | 27 | There are about twenty thousand more lines of unpublished code in the 28 | background consisting of the previous iterations and other support scripts 29 | and tools that needed to be written to get the job done. 30 | 31 | # Accuracy 32 | 33 | When running, `si78c` produces identical memory states (apart from the stack) to 34 | the original version. As a natural side effect, it produces pixel accurate 35 | frames compared to the original. 36 | 37 | Cycle timing is not particularly accurate, but the game code is not 38 | particularly sensitive to this, as it uses interrupts for timing most 39 | things, instead of clock cycles. 40 | 41 | # Audience 42 | 43 | The intended audience is hackers, enthusiasts, scholars, students, historians, 44 | and anyone else engaged in digital archaeology. 45 | 46 | The code is intended to be used as a more accessible guide to studying the 47 | original game, and learning about its inner workings. 48 | 49 | # Conventions 50 | 51 | Where practical, I have used the same or similar function and variable names as 52 | Cantrell, to more easily aid people studying both versions. The code is also 53 | laid out in a similar order to the original. 54 | 55 | Every function with a matching analog in the original system is signposted 56 | with a comment like `xref 028e`, which gives the address of the matching routine 57 | in the original ROM. 58 | 59 | A few other places in the code like loops and branches are annotated similarly. 60 | 61 | To find more detail on code near an xref, you can use Cantrell's 62 | excellent notes [here](https://computerarcheology.com/Arcade/SpaceInvaders/Code.html). 63 | 64 | # Threading 65 | 66 | The original code is interrupt driven, and partially co-operatively 67 | multitasked. The game spends about a third of the time running the main 68 | thread, which gets pre-empted by the midscreen and vblank interrupts. The other 69 | two thirds of the time is split between those interrupt contexts, which are not 70 | pre-empted, but decide when to return to main. 71 | 72 | It also contains some interesting parts like this: 73 | 74 | ``` 75 | 02D0: 31 00 24 LD SP,$2400 // wipe the stack 76 | 02D3: FB EI // drop isr context 77 | 02D4: CD D7 19 CALL DsableGameTasks // keep going from this point 78 | ``` 79 | 80 | That code (running in an interrupt context) after detecting the player's death, 81 | essentially wipes out all thread contexts (including itself), and then becomes 82 | the new main context. 83 | 84 | To handle things like this in the this in si78c, I decided to use `ucontext` 85 | (user level threading) to give me more fine grained control over thread 86 | switching, creation and destruction. 87 | 88 | The equivalent piece of code in C to the above is a bit messier, and involves 89 | using `swapcontext` to swap to a scheduler context, which then resets all the 90 | contexts, and then swaps back and re-enters at the desired point. 91 | 92 | # Limitations 93 | 94 | There is no sound, as the sound hardware is not emulated. 95 | 96 | Cycle timing is not particularly accurate, as mentioned, but its not very 97 | important in this case. 98 | 99 | The code will currently only work on little endian systems, as the original 100 | system (8080) was little endian, and we use the ROM data as is. 101 | 102 | # Porting 103 | 104 | The game is known to build and run on Ubuntu 18.04 and MacOSX El Capitan, and 105 | will likely run on other x86 Unix systems, as long as they support `ucontext` and 106 | `SDL2`. Unfortunately, `ucontext` is deprecated on MacOSX, so it may not run on 107 | later versions. 108 | 109 | The game is written in the subset of C99 that is compatible with C++, and uses 110 | no compiler extensions apart from attribute packed. It will build fine on GCC 3 111 | and above, and most likely any Unix C or C++ compiler newer than that. 112 | 113 | Porting to a non unix system like Windows would require changing out `ucontext` 114 | to use threads or fibers. Porting to a simpler system like DOS, or something 115 | bare metal will require writing some code to blit to the framebuffer, and 116 | adapting to whatever native interrupt facilities are available. 117 | 118 | In terms of CPU grunt required, the code isn't particularly optimized, and 119 | currently requires a 32-bit processor capable of at least 10 Mips. It could be 120 | made to fit on a smaller system with a bit of work. 121 | 122 | The code assumes little endian. To port to a big endian system would require 123 | further dissection of the ROM, to identify and swizzle any little endian data 124 | when it is loaded. 125 | 126 | Porting the code to another language would not be a small task, and would most 127 | likely require switching out the threading system, and converting the code to 128 | not use pointers. 129 | 130 | # Building and running 131 | 132 | `SDL2` is required as a dependency, to install on Ubuntu, do this: 133 | 134 | $ sudo apt-get install libsdl2-dev 135 | 136 | To grab the code and build the binary, do this: 137 | 138 | $ git clone https://github.com/loadzero/si78c.git && cd si78c 139 | $ make 140 | 141 | As mentioned, the original arcade ROM is required, `invaders.zip` from the 142 | `MAME_078` set is known to work. 143 | 144 | The constituent parts must be placed in a folder called `inv1`, and have 145 | these checksums: 146 | 147 | $ md5sum inv1/* 148 | 149 | 7d3b201f3e84af3b4fcb8ce8619ec9c6 inv1/invaders.e 150 | 7709a2576adb6fedcdfe175759e5c17a inv1/invaders.f 151 | 9ec2dc89315a0d50c5e166f664f64a48 inv1/invaders.g 152 | e87815985f5208bfa25d567c3fb52418 inv1/invaders.h 153 | 154 | To run it: 155 | 156 | $ ./bin/si78c 157 | 158 | The keyboard controls are: 159 | 160 | a LEFT 161 | d RIGHT 162 | 1 1P 163 | 2 2P 164 | j FIRE 165 | 5 COIN 166 | t TILT 167 | 168 | -------------------------------------------------------------------------------- /si78c.c: -------------------------------------------------------------------------------- 1 | // 2 | // Space Invaders 1978 in C 3 | // Jason McSweeney 4 | 5 | #define _XOPEN_SOURCE 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define OP_BLEND 0 18 | #define OP_ERASE 1 19 | #define OP_COLLIDE 2 20 | #define OP_BLIT 3 21 | 22 | #define SHOT_ACTIVE 0x80 23 | #define SHOT_BLOWUP 0x1 24 | 25 | #define BEAM_VBLANK 0x80 26 | #define BEAM_MIDDLE 0x00 27 | #define XR_MID 0x80 28 | 29 | #define DIP3_SHIPS1 0x1 30 | #define DIP5_SHIPS2 0x2 31 | #define DIP6_BONUS 0x8 32 | #define DIP7_COININFO 0x80 33 | 34 | #define TILT_BIT 0x4 35 | #define COIN_BIT 0x1 36 | 37 | #define INIT 1 38 | #define PROMPT 2 39 | #define SHIELDS 4 40 | 41 | #define ROL_SHOT_PICEND 0xf9 42 | #define PLU_SHOT_PICEND 0xed 43 | #define SQU_SHOT_PICEND 0xdb 44 | 45 | #define PLAYER_ADDR 0x2010 46 | #define PLAYER_SIZE 16 47 | #define PLAYER_SHOT_ADDR 0x2020 48 | #define PLAYER_SHOT_DATA_ADDR 0x2025 49 | #define PLAYER_SHOT_DATA_SIZE 7 50 | #define ROLLING_SHOT_ADDR 0x2030 51 | #define ROLLING_SHOT_SIZE 16 52 | #define PLUNGER_SHOT_ADDR 0x2040 53 | #define PLUNGER_SHOT_SIZE 16 54 | #define SQUIGGLY_SHOT_ADDR 0x2050 55 | #define SQUIGGLY_SHOT_SIZE 16 56 | #define SAUCER_ADDR 0x2083 57 | #define SAUCER_SIZE 10 58 | #define P1_ADDR 0x2100 59 | #define P2_ADDR 0x2200 60 | #define SPLASH_DESC_ADDR 0x20c5 61 | 62 | #if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ 63 | #error "This code is Little Endian only." 64 | #endif 65 | 66 | struct Word 67 | { 68 | union { 69 | 70 | uint16_t u16; 71 | struct { 72 | uint8_t l; 73 | uint8_t h; 74 | }; 75 | 76 | struct { 77 | uint8_t y; 78 | uint8_t x; 79 | }; 80 | 81 | }; 82 | } __attribute__ ((packed)); // 4 83 | 84 | typedef struct Word Word; 85 | 86 | struct SprDesc 87 | { 88 | Word spr; 89 | 90 | union { 91 | Word pos; // pixel position 92 | Word sc; // screen address 93 | }; 94 | 95 | uint8_t n; 96 | 97 | } __attribute__ ((packed)); // 5 98 | 99 | typedef struct SprDesc SprDesc; 100 | 101 | struct GameObjHeader 102 | { 103 | uint8_t TimerMSB; 104 | uint8_t TimerLSB; 105 | uint8_t TimerExtra; 106 | Word Handler; 107 | 108 | } __attribute__ ((packed)); // 5 109 | 110 | typedef struct GameObjHeader GameObjHeader; 111 | 112 | struct AShot 113 | { 114 | uint8_t Status; 115 | uint8_t StepCnt; 116 | uint8_t Track; 117 | Word CFir; 118 | uint8_t BlowCnt; 119 | SprDesc Desc; 120 | 121 | } __attribute__ ((packed)); // 11 122 | 123 | typedef struct AShot AShot; 124 | 125 | struct Mem { 126 | 127 | uint8_t pad_01[3063]; // 0x0000 128 | uint8_t MSG_TAITO_COP[9]; // 0x0bf7 129 | uint8_t pad_02[3601]; // 0x0c00 130 | uint8_t soundDelayKey[16]; // 0x1a11 131 | uint8_t soundDelayValue[16]; // 0x1a21 132 | uint8_t pad_03[112]; // 0x1a31 133 | uint8_t ShotReloadRate[5]; // 0x1aa1 134 | uint8_t MSG_GAME_OVER__PLAYER___[20]; // 0x1aa6 135 | uint8_t MSG_1_OR_2PLAYERS_BUTTON[20]; // 0x1aba 136 | uint8_t pad_04; // 0x1ace 137 | uint8_t MSG_ONLY_1PLAYER__BUTTON[20]; // 0x1acf 138 | uint8_t pad_05; // 0x1ae3 139 | uint8_t MSG__SCORE_1__HI_SCORE_SCORE_2__[28]; // 0x1ae4 140 | uint8_t pad_06[112]; // 0x1b00 141 | uint8_t MSG_PLAY_PLAYER_1_[14]; // 0x1b70 142 | uint8_t pad_07[66]; // 0x1b7e 143 | uint8_t SPLASH_SHOT_OBJDATA[16]; // 0x1bc0 144 | uint8_t pad_08[144]; // 0x1bd0 145 | uint8_t PLAYER_SPRITES[48]; // 0x1c60 146 | uint8_t pad_09[9]; // 0x1c90 147 | uint8_t MSG__10_POINTS[10]; // 0x1c99 148 | uint8_t MSG__SCORE_ADVANCE_TABLE_[21]; // 0x1ca3 149 | uint8_t AReloadScoreTab[4]; // 0x1cb8 150 | uint8_t MSG_TILT[4]; // 0x1cbc 151 | uint8_t pad_10[28]; // 0x1cc0 152 | uint8_t AlienShotExplodingSprite[6]; // 0x1cdc 153 | uint8_t pad_11[24]; // 0x1ce2 154 | uint8_t MSG_PLAY2[5]; // 0x1cfa 155 | uint8_t pad_12[33]; // 0x1cff 156 | uint8_t SHIELD_SPRITE[44]; // 0x1d20 157 | uint8_t SauScrValueTab[4]; // 0x1d4c 158 | uint8_t SauScrStrTab[4]; // 0x1d50 159 | uint8_t pad_13[40]; // 0x1d54 160 | uint8_t SpriteSaucerExp[24]; // 0x1d7c 161 | uint8_t MSG__50[3]; // 0x1d94 162 | uint8_t MSG_100[3]; // 0x1d97 163 | uint8_t MSG_150[3]; // 0x1d9a 164 | uint8_t MSG_300[3]; // 0x1d9d 165 | uint8_t AlienScores[3]; // 0x1da0 166 | uint8_t AlienStartTable[8]; // 0x1da3 167 | uint8_t MSG_PLAY[4]; // 0x1dab 168 | uint8_t MSG_SPACE__INVADERS[15]; // 0x1daf 169 | uint8_t SCORE_ADV_SPRITE_LIST[17]; // 0x1dbe 170 | uint8_t SCORE_ADV_MSG_LIST[17]; // 0x1dcf 171 | uint8_t MSG____MYSTERY[10]; // 0x1de0 172 | uint8_t MSG__30_POINTS[10]; // 0x1dea 173 | uint8_t MSG__20_POINTS[10]; // 0x1df4 174 | uint8_t pad_14[338]; // 0x1dfe 175 | uint8_t MSG__1_OR_2_PLAYERS___[18]; // 0x1f50 176 | uint8_t pad_15[46]; // 0x1f62 177 | uint8_t MSG_INSERT__COIN[12]; // 0x1f90 178 | uint8_t CREDIT_TABLE[4]; // 0x1f9c 179 | uint8_t CREDIT_TABLE_COINS[9]; // 0x1fa0 180 | uint8_t MSG_CREDIT_[7]; // 0x1fa9 181 | uint8_t pad_16[67]; // 0x1fb0 182 | uint8_t MSG_PUSH[4]; // 0x1ff3 183 | uint8_t pad_17[9]; // 0x1ff7 184 | 185 | // start of ram mirror 186 | 187 | uint8_t waitOnDraw; // 0x2000 188 | uint8_t pad_18; // 0x2001 189 | uint8_t alienIsExploding; // 0x2002 190 | uint8_t expAlienTimer; // 0x2003 191 | uint8_t alienRow; // 0x2004 192 | uint8_t alienFrame; // 0x2005 193 | uint8_t alienCurIndex; // 0x2006 194 | Word refAlienDelta; // 0x2007 195 | Word refAlienPos; // 0x2009 196 | Word alienPos; // 0x200b 197 | uint8_t rackDirection; // 0x200d 198 | uint8_t rackDownDelta; // 0x200e 199 | uint8_t pad_19; // 0x200f 200 | GameObjHeader playerHeader; // 0x2010 201 | uint8_t playerAlive; // 0x2015 202 | uint8_t expAnimateTimer; // 0x2016 203 | uint8_t expAnimateCnt; // 0x2017 204 | SprDesc playerDesc; // 0x2018 205 | uint8_t nextDemoCmd; // 0x201d 206 | uint8_t hidMessSeq; // 0x201e 207 | uint8_t pad_20; // 0x201f 208 | GameObjHeader plyrShotHeader; // 0x2020 209 | uint8_t plyrShotStatus; // 0x2025 210 | uint8_t blowUpTimer; // 0x2026 211 | SprDesc playerShotDesc; // 0x2027 212 | uint8_t shotDeltaYr; // 0x202c 213 | uint8_t fireBounce; // 0x202d 214 | uint8_t pad_21[2]; // 0x202e 215 | GameObjHeader rolShotHeader; // 0x2030 216 | AShot rolShotData; // 0x2035 217 | GameObjHeader pluShotHeader; // 0x2040 218 | AShot pluShotData; // 0x2045 219 | GameObjHeader squShotHeader; // 0x2050 220 | AShot squShotData; // 0x2055 221 | uint8_t pad_22; // 0x2060 222 | uint8_t collision; // 0x2061 223 | SprDesc expAlien; // 0x2062 224 | uint8_t playerDataMSB; // 0x2067 225 | uint8_t playerOK; // 0x2068 226 | uint8_t enableAlienFire; // 0x2069 227 | uint8_t alienFireDelay; // 0x206a 228 | uint8_t pad_23; // 0x206b 229 | uint8_t temp206C; // 0x206c 230 | uint8_t invaded; // 0x206d 231 | uint8_t skipPlunger; // 0x206e 232 | uint8_t pad_24; // 0x206f 233 | uint8_t otherShot1; // 0x2070 234 | uint8_t otherShot2; // 0x2071 235 | uint8_t vblankStatus; // 0x2072 236 | AShot aShot; // 0x2073 237 | uint8_t alienShotDelta; // 0x207e 238 | uint8_t shotPicEnd; // 0x207f 239 | uint8_t shotSync; // 0x2080 240 | uint8_t tmp2081; // 0x2081 241 | uint8_t numAliens; // 0x2082 242 | uint8_t saucerStart; // 0x2083 243 | uint8_t saucerActive; // 0x2084 244 | uint8_t saucerHit; // 0x2085 245 | uint8_t saucerHitTime; // 0x2086 246 | SprDesc saucerDesc; // 0x2087 247 | uint8_t saucerDXr; // 0x208c 248 | Word sauScore; // 0x208d 249 | Word shotCount; // 0x208f 250 | Word saucerTimer; // 0x2091 251 | uint8_t waitStartLoop; // 0x2093 252 | uint8_t soundPort3; // 0x2094 253 | uint8_t changeFleetSnd; // 0x2095 254 | uint8_t fleetSndCnt; // 0x2096 255 | uint8_t fleetSndReload; // 0x2097 256 | uint8_t soundPort5; // 0x2098 257 | uint8_t extraHold; // 0x2099 258 | uint8_t tilt; // 0x209a 259 | uint8_t fleetSndHold; // 0x209b 260 | uint8_t pad_25[36]; // 0x209c 261 | 262 | // end of partial ram restore at 0x20c0 263 | 264 | uint8_t isrDelay; // 0x20c0 265 | uint8_t isrSplashTask; // 0x20c1 266 | uint8_t splashAnForm; // 0x20c2 267 | Word splashDelta; // 0x20c3 268 | Word splashPos; // 0x20c5 269 | Word splashPic; // 0x20c7 270 | uint8_t splashPicSize; // 0x20c9 271 | uint8_t splashTargetX; // 0x20ca 272 | uint8_t splashReached; // 0x20cb 273 | Word splashImRest; // 0x20cc 274 | uint8_t twoPlayers; // 0x20ce 275 | uint8_t aShotReloadRate; // 0x20cf 276 | uint8_t pad_26[21]; // 0x20d0 277 | Word playerExtras; // 0x20e5 278 | Word playerStates; // 0x20e7 279 | uint8_t gameTasksRunning; // 0x20e9 280 | uint8_t coinSwitch; // 0x20ea 281 | uint8_t numCoins; // 0x20eb 282 | uint8_t splashAnimate; // 0x20ec 283 | Word demoCmdPtr; // 0x20ed 284 | uint8_t gameMode; // 0x20ef 285 | uint8_t pad_27; // 0x20f0 286 | uint8_t adjustScore; // 0x20f1 287 | Word scoreDelta; // 0x20f2 288 | Word HiScor; // 0x20f4 289 | uint8_t pad_28[2]; // 0x20f6 290 | Word P1Scor; // 0x20f8 291 | uint8_t pad_29[2]; // 0x20fa 292 | Word P2Scor; // 0x20fc 293 | uint8_t pad_30[68]; // 0x20fe 294 | 295 | // end of ram mirror at 0x2100 296 | 297 | uint8_t p1ShieldBuffer[176]; // 0x2142 298 | uint8_t pad_31[9]; // 0x21f2 299 | uint8_t p1RefAlienDX; // 0x21fb 300 | Word p1RefAlienPos; // 0x21fc 301 | uint8_t p1RackCnt; // 0x21fe 302 | uint8_t p1ShipsRem; // 0x21ff 303 | uint8_t pad_32[66]; // 0x2200 304 | uint8_t p2ShieldBuffer[176]; // 0x2242 305 | uint8_t pad_33[9]; // 0x22f2 306 | uint8_t p2RefAlienDX; // 0x22fb 307 | Word p2RefAlienPos; // 0x22fc 308 | uint8_t p2RackCnt; // 0x22fe 309 | uint8_t p2ShipsRem; // 0x22ff 310 | uint8_t pad_34[256]; // 0x2300 311 | uint8_t vram[7168]; // 0x2400 312 | 313 | // Technically the region below is supposed to be a mirror, but AFAICT, 314 | // that property is not used by the SI code. 315 | // 316 | // The 'PLAy' animation does do some oob writes during DrawSprite, 317 | // which effectively do nothing because they end up trying to write to ROM 318 | // 319 | // So, as a catchall, we just reserve this area up to the end of the address space. 320 | 321 | uint8_t oob[49152]; // 0x4000 322 | 323 | } __attribute ((packed)); 324 | 325 | typedef struct Mem Mem; 326 | 327 | typedef struct PriCursor 328 | { 329 | uint8_t* src; 330 | Word sc; 331 | uint8_t* obj; 332 | } PriCursor; 333 | 334 | typedef struct ShieldBufferCursor 335 | { 336 | Word sc; 337 | uint8_t* iter; 338 | } ShieldBufferCursor; 339 | 340 | typedef enum YieldReason 341 | { 342 | YIELD_INIT = 0, 343 | YIELD_TIMESLICE, 344 | YIELD_INTFIN, 345 | YIELD_WAIT_FOR_START, 346 | YIELD_PLAYER_DEATH, 347 | YIELD_INVADED, 348 | YIELD_TILT, 349 | YIELD_UNKNOWN, 350 | } YieldReason; 351 | 352 | enum Keys { 353 | KEYS_LEFT = 1, 354 | KEYS_RIGHT = 2, 355 | KEYS_START = 4, 356 | KEYS_START2 = 8, 357 | KEYS_FIRE = 16, 358 | KEYS_COIN = 32, 359 | KEYS_TILT = 64, 360 | KEYS_DIP6 = 128, 361 | KEYS_DIP7 = 256, 362 | KEYS_SPECIAL1 = 512, 363 | KEYS_SPECIAL2 = 1024, 364 | KEYS_QUIT = 2048 365 | }; 366 | 367 | #define KEY_LIST \ 368 | KEY_MAP('a', KEYS_LEFT); \ 369 | KEY_MAP('d', KEYS_RIGHT); \ 370 | KEY_MAP('1', KEYS_START); \ 371 | KEY_MAP('2', KEYS_START2); \ 372 | KEY_MAP('j', KEYS_FIRE); \ 373 | KEY_MAP('5', KEYS_COIN); \ 374 | KEY_MAP('t', KEYS_TILT); \ 375 | KEY_MAP('6', KEYS_DIP6); \ 376 | KEY_MAP('7', KEYS_DIP7); \ 377 | KEY_MAP('z', KEYS_SPECIAL1); \ 378 | KEY_MAP('x', KEYS_SPECIAL2); \ 379 | KEY_MAP(SDLK_ESCAPE, KEYS_QUIT); 380 | 381 | #define TRUE 1 382 | #define FALSE 0 383 | 384 | static void do_logprintf(const char *file, unsigned line, const char* format, ...); 385 | #define logprintf(...) { do_logprintf(__FILE__, __LINE__, __VA_ARGS__); } 386 | 387 | #include "si78c_proto.h" 388 | 389 | // Coordinate Systems 390 | // ------------------ 391 | // 392 | // Natural Units 393 | // ------------- 394 | // 395 | // For readability, this codebase uses the following coordinate system 396 | // where possible. 397 | // 398 | // The origin is at the bottom left corner. 399 | // 400 | // X goes +ve towards the rhs of the screen. 401 | // Y goes +ve towards the top of the screen. 402 | // 403 | // (0,256) 404 | // ^ 405 | // | 406 | // y | 407 | // | 408 | // |----------> (224,0) 409 | // (0,0) x 410 | // 411 | // Game Units 412 | // ------------- 413 | // 414 | // The game uses two different coordinate systems, both of which 415 | // fit into a 16-bit word, and come with an offset. 416 | // 417 | // pix - Pixel positions between 0x2000 and 0xffff 418 | // sc - Screen RAM addresses between 0x2400 and 0x3fff 419 | // 420 | // pix coordinates are used to move and draw objects that require per pixel shifting, 421 | // like the aliens and the bullets. 422 | // 423 | // sc coordinates are used for drawing text and other simple sprites, and also when 424 | // blitting sprites after they have been shifted. 425 | // 426 | // The following macros are used to convert between natural units and game units. 427 | 428 | #define xysc(x, y) ((x)*32 + (y)/8) 429 | #define xytosc(x, y) u16_to_word(0x2400 + xysc((x),(y))) 430 | #define xpix(x) ((x)+32) 431 | #define xytopix(x, y) u16_to_word((xpix((x)) << 8) | (y)) 432 | 433 | #define STACK_SIZE 65536 434 | 435 | #define CRED1 17152 436 | #define CRED2 16384 437 | 438 | static Mem m; 439 | static uint8_t *rawmem = (uint8_t*) &m; 440 | 441 | static int64_t ticks; 442 | static int im; 443 | static int irq_state; 444 | static int irq_vector; 445 | 446 | static uint16_t shift_data; 447 | static uint8_t shift_count; 448 | 449 | static ucontext_t frontend_ctx; 450 | static ucontext_t main_ctx; 451 | static ucontext_t int_ctx; 452 | 453 | static ucontext_t *prev_ctx; 454 | static ucontext_t *curr_ctx; 455 | 456 | static uint8_t main_ctx_stack[STACK_SIZE]; 457 | static uint8_t int_ctx_stack[STACK_SIZE]; 458 | 459 | static YieldReason yield_reason; 460 | 461 | static SDL_Window *window; 462 | static SDL_Renderer *renderer; 463 | 464 | static const int renderscale = 2; 465 | 466 | static uint64_t keystate; 467 | 468 | static int exited; 469 | 470 | static uint8_t port1; 471 | static uint8_t port2; 472 | 473 | int main(int argc, char **argv) 474 | { 475 | init_renderer(); 476 | init_game(); 477 | 478 | int credit = 0; 479 | size_t frame = -1; 480 | 481 | while (1) 482 | { 483 | frame++; 484 | input(); 485 | 486 | if (exited) break; 487 | 488 | // preserves timing compatibility with MAME 489 | if (frame == 1) 490 | credit--; 491 | 492 | // up to mid 493 | 494 | credit += CRED1; 495 | loop_core(&credit); 496 | irq(0xcf); 497 | 498 | // up to vblank 499 | 500 | credit += CRED2; 501 | loop_core(&credit); 502 | irq(0xd7); 503 | 504 | render(); 505 | } 506 | 507 | fini_game(); 508 | fini_renderer(); 509 | 510 | return 0; 511 | } 512 | 513 | static void input() 514 | { 515 | SDL_Event event_buffer[64]; 516 | size_t num = 0; 517 | 518 | while (num < 64) 519 | { 520 | int has = SDL_PollEvent(&event_buffer[num]); 521 | if (!has) break; 522 | num++; 523 | } 524 | 525 | for (size_t i = 0; i < num; ++i) 526 | { 527 | SDL_Event e = event_buffer[i]; 528 | 529 | if (e.type == SDL_QUIT) 530 | { 531 | e.type = SDL_KEYDOWN; 532 | e.key.keysym.sym = SDLK_ESCAPE; 533 | } 534 | 535 | if (! (e.type == SDL_KEYDOWN || e.type == SDL_KEYUP)) continue; 536 | 537 | uint64_t mask = 0; 538 | uint64_t f = e.type == SDL_KEYDOWN; 539 | 540 | switch (e.key.keysym.sym) 541 | { 542 | #define KEY_MAP(x, y) case x: mask = y; break; 543 | KEY_LIST 544 | #undef KEY_MAP 545 | } 546 | 547 | keystate = (keystate & ~mask) | (-f & mask); 548 | } 549 | 550 | #define BIT(x) (!!(keystate & (x))) 551 | 552 | port1 = (BIT(KEYS_RIGHT) << 6) | 553 | (BIT(KEYS_LEFT) << 5) | 554 | (BIT(KEYS_FIRE) << 4) | 555 | (1 << 3) | 556 | (BIT(KEYS_START) << 2) | 557 | (BIT(KEYS_START2) << 1) | 558 | (BIT(KEYS_COIN) << 0); 559 | 560 | port2 = (BIT(KEYS_DIP7) << 7) | 561 | (BIT(KEYS_RIGHT) << 6) | 562 | (BIT(KEYS_LEFT) << 5) | 563 | (BIT(KEYS_FIRE) << 4) | 564 | (BIT(KEYS_DIP6) << 3) | 565 | (BIT(KEYS_TILT) << 2); 566 | 567 | exited = BIT(KEYS_QUIT); 568 | } 569 | 570 | static void init_renderer() 571 | { 572 | int rc = SDL_Init(SDL_INIT_EVERYTHING); 573 | assert(rc == 0); 574 | 575 | window = SDL_CreateWindow("si78c", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 576 | 224 * renderscale, 256 * renderscale, 0); 577 | assert(window); 578 | 579 | SDL_ShowCursor(SDL_DISABLE); 580 | 581 | renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); 582 | assert(renderer); 583 | } 584 | 585 | static void fini_renderer() 586 | { 587 | SDL_DestroyRenderer(renderer); 588 | SDL_DestroyWindow(window); 589 | } 590 | 591 | static void render() 592 | { 593 | SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); 594 | SDL_RenderClear(renderer); 595 | SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); 596 | 597 | const uint8_t *iter = rawmem + 0x2400; 598 | 599 | for (int y = 0; y < 224; ++y) 600 | { 601 | for (int xi = 0; xi < 32; ++xi) 602 | { 603 | uint8_t byte = *iter++; 604 | 605 | for (int i = 0; i < 8; ++i) 606 | { 607 | int x = xi * 8 + i; 608 | int on = (byte >> i) & 0x1; 609 | 610 | if (on) 611 | { 612 | SDL_Rect rect; 613 | rect.x = y * renderscale; 614 | rect.y = (256 - x - 1) * renderscale; 615 | rect.w = renderscale; 616 | rect.h = renderscale; 617 | 618 | SDL_RenderDrawRect(renderer, &rect); 619 | } 620 | } 621 | } 622 | } 623 | 624 | SDL_RenderPresent(renderer); 625 | } 626 | 627 | static void loop_core(int *credit) 628 | { 629 | int allowed = 1; 630 | 631 | while (*credit > 0) 632 | *credit -= execute(allowed); 633 | } 634 | 635 | static void init_game() 636 | { 637 | assert(sizeof(m) == 0x10000); 638 | load_rom(&m); 639 | 640 | assert(checksum(&m) == 0x6dfbd7cc); 641 | init_threads(YIELD_INIT); 642 | } 643 | 644 | static void fini_game() 645 | { 646 | } 647 | 648 | static void init_threads(YieldReason entry_point) 649 | { 650 | int rc = getcontext(&main_ctx); 651 | assert(rc == 0); 652 | 653 | main_ctx.uc_stack.ss_sp = main_ctx_stack; 654 | main_ctx.uc_stack.ss_size = STACK_SIZE; 655 | main_ctx.uc_link = &frontend_ctx; 656 | 657 | makecontext(&main_ctx, (void (*)()) run_main_ctx, 1, entry_point); 658 | 659 | rc = getcontext(&int_ctx); 660 | 661 | int_ctx.uc_stack.ss_sp = &int_ctx_stack; 662 | int_ctx.uc_stack.ss_size = STACK_SIZE; 663 | int_ctx.uc_link = &frontend_ctx; 664 | 665 | makecontext(&int_ctx, run_int_ctx, 0); 666 | prev_ctx = &main_ctx; 667 | curr_ctx = &frontend_ctx; 668 | } 669 | 670 | static void run_main_ctx(YieldReason entry) 671 | { 672 | switch(entry) 673 | { 674 | case YIELD_INIT: reset(); break; 675 | case YIELD_WAIT_FOR_START: WaitForStart(); break; 676 | case YIELD_PLAYER_DEATH: player_death(0); break; 677 | case YIELD_INVADED: on_invaded(); break; 678 | case YIELD_TILT: on_tilt(); break; 679 | default: assert(FALSE); 680 | } 681 | } 682 | 683 | static void run_int_ctx() 684 | { 685 | while (1) 686 | { 687 | // 0xcf = RST 1 opcode (call 0x8) 688 | // 0xd7 = RST 2 opcode (call 0x16) 689 | 690 | if (irq_vector == 0xcf) 691 | midscreen_int(); 692 | else if (irq_vector == 0xd7) 693 | vblank_int(); 694 | 695 | enable_interrupts(); 696 | yield(YIELD_INTFIN); 697 | } 698 | } 699 | 700 | static unsigned checksum(Mem *m) 701 | { 702 | assert(sizeof(*m) == 0x10000); 703 | assert((uintptr_t) m % 4 == 0); 704 | unsigned *ptr = (unsigned*) m; 705 | size_t n = sizeof(*m) / 4; 706 | 707 | unsigned sum = 0; 708 | 709 | for (size_t i = 0; i < n; ++i) 710 | sum += ptr[i]; 711 | 712 | return sum; 713 | } 714 | 715 | static void rom_load(void *mem, const char* name, size_t offset, size_t len) 716 | { 717 | char fbuf[256]; sprintf(fbuf, "inv1/%s", name); 718 | 719 | FILE *romfile = fopen(fbuf, "r"); 720 | assert(romfile); 721 | 722 | ssize_t rn = fread((char*) mem + offset, 1, len, romfile); 723 | assert((size_t) rn == len); 724 | fclose(romfile); 725 | } 726 | 727 | static void load_rom(void *mem) 728 | { 729 | rom_load(mem, "invaders.h", 0x0000, 0x0800); 730 | rom_load(mem, "invaders.g", 0x0800, 0x0800); 731 | rom_load(mem, "invaders.f", 0x1000, 0x0800); 732 | rom_load(mem, "invaders.e", 0x1800, 0x0800); 733 | } 734 | 735 | static int execute(int allowed) 736 | { 737 | int64_t start = ticks; 738 | 739 | ucontext_t *next = NULL; 740 | 741 | switch (yield_reason) 742 | { 743 | case YIELD_INIT: 744 | case YIELD_TIMESLICE: 745 | next = prev_ctx; 746 | break; 747 | case YIELD_INTFIN: 748 | next = &main_ctx; 749 | break; 750 | case YIELD_PLAYER_DEATH: 751 | case YIELD_WAIT_FOR_START: 752 | case YIELD_INVADED: 753 | init_threads(yield_reason); 754 | enable_interrupts(); 755 | next = &main_ctx; 756 | break; 757 | case YIELD_TILT: 758 | init_threads(yield_reason); 759 | next = &main_ctx; 760 | break; 761 | default: 762 | assert(FALSE); 763 | } 764 | 765 | yield_reason = YIELD_UNKNOWN; 766 | 767 | if (allowed && interrupted()) 768 | { 769 | next = &int_ctx; 770 | } 771 | 772 | switch_to(next); 773 | 774 | return ticks - start; 775 | } 776 | 777 | static void switch_to(ucontext_t *to) 778 | { 779 | co_switch(curr_ctx, to); 780 | } 781 | 782 | static void co_switch(ucontext_t *prev, ucontext_t *next) 783 | { 784 | prev_ctx = prev; 785 | curr_ctx = next; 786 | 787 | swapcontext(prev, next); 788 | } 789 | 790 | static void timeslice() 791 | { 792 | ticks += 30; 793 | 794 | yield(YIELD_TIMESLICE); 795 | } 796 | 797 | static void yield(YieldReason reason) 798 | { 799 | yield_reason = reason; 800 | switch_to(&frontend_ctx); 801 | } 802 | 803 | static uint8_t get_input(int64_t ticks, uint8_t port) 804 | { 805 | if (port == 1) 806 | return port1; 807 | 808 | if (port == 2) 809 | return port2; 810 | 811 | fatalerror("unknown port %d\n", port); 812 | return 0; 813 | } 814 | 815 | static uint8_t read_port(uint8_t port) 816 | { 817 | if (port == 3) 818 | return (shift_data << shift_count) >> 8; 819 | 820 | uint8_t val = get_input(ticks, port); 821 | return val; 822 | } 823 | 824 | static void write_port(uint16_t port, uint8_t v) 825 | { 826 | if (port == 2) 827 | { 828 | shift_count = v & 0x7; 829 | } 830 | else if (port == 4) 831 | { 832 | shift_data = (v << 8) | (shift_data >> 8); 833 | } 834 | 835 | timeslice(); 836 | } 837 | 838 | static void enable_interrupts() 839 | { 840 | im = 1; 841 | } 842 | 843 | static void irq(uint8_t v) 844 | { 845 | irq_vector = v; 846 | irq_state = 1; 847 | } 848 | 849 | static int interrupted() 850 | { 851 | // The two interrupts correspond to midscreen, and start of vblank. 852 | // 0xcf = RST 1 opcode (call 0x8) 853 | // 0xd7 = RST 2 opcode (call 0x16) 854 | 855 | if (irq_state && im) 856 | { 857 | assert(irq_vector == 0xcf || irq_vector == 0xd7); 858 | 859 | irq_state = 0; 860 | im = 0; 861 | return TRUE; 862 | } 863 | 864 | return FALSE; 865 | } 866 | 867 | static void fatalerror(const char* format, ...) 868 | { 869 | va_list ap; 870 | va_start(ap, format); 871 | vfprintf(stdout, format, ap); 872 | va_end(ap); 873 | 874 | fflush(stdout); 875 | fflush(stderr); 876 | 877 | exit(1); 878 | } 879 | 880 | static inline Word u16_to_word(uint16_t u) 881 | { 882 | Word w; w.u16 = u; return w; 883 | } 884 | 885 | static inline Word u8_u8_to_word(uint8_t h, uint8_t l) 886 | { 887 | return u16_to_word((h << 8) | l); 888 | } 889 | 890 | static inline uint16_t ptr_to_u16(uint8_t *ptr) 891 | { 892 | return (uint16_t) (ptr - (uint8_t*) &m); 893 | } 894 | 895 | static inline Word ptr_to_word(uint8_t *ptr) 896 | { 897 | return u16_to_word(ptr_to_u16(ptr)); 898 | } 899 | 900 | static inline uint8_t* u16_to_ptr(uint16_t u) 901 | { 902 | return ((uint8_t*) &m) + u; 903 | } 904 | 905 | static inline uint8_t* word_to_ptr(Word w) 906 | { 907 | return u16_to_ptr(w.u16); 908 | } 909 | 910 | static int is_godmode() 911 | { 912 | uint16_t addr = 0x060f; 913 | uint8_t nops[] = {0,0,0}; 914 | 915 | return memcmp(((uint8_t*) &m) + addr, nops, 3) == 0; 916 | } 917 | 918 | static uint8_t* rompos(uint8_t* ram) 919 | { 920 | return ram - 0x500; 921 | } 922 | 923 | static uint8_t bcd_add(uint8_t bcd, uint8_t a, uint8_t *carry) 924 | { 925 | // Add the given number to the given bcd value, as per ADI / ADDB etc 926 | int q = bcd + a + *carry; 927 | *carry = (q >> 8) & 0x1; 928 | int aux = (bcd ^ q ^ a) & 0x10; 929 | bcd = q; 930 | 931 | // Adjust the result back into bcd as per DAA 932 | uint8_t w = bcd; 933 | 934 | if (aux || ((bcd & 0xf) > 9)) w += 6; 935 | if ((*carry) || (bcd > 0x99)) w += 0x60; 936 | *carry |= bcd > 0x99; 937 | bcd = w; 938 | 939 | return bcd; 940 | } 941 | 942 | static void do_logprintf(const char *file, unsigned line, const char* format, ...) 943 | { 944 | fprintf(stdout, "%s:%d: ", file, line); 945 | 946 | va_list ap; 947 | va_start(ap, format); 948 | vfprintf(stdout, format, ap); 949 | va_end(ap); 950 | 951 | fflush(stdout); 952 | } 953 | 954 | static void DebugMessage(Word sc, uint8_t* msg, uint8_t n) 955 | { 956 | uint16_t raw = sc.u16 - 0x2400; 957 | 958 | uint16_t x = raw / 32; 959 | uint16_t y = (raw % 32) * 8; 960 | static const char *alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789<> =*...............?......-"; 961 | 962 | char sbuf[256]; 963 | 964 | for (size_t i = 0; i < n; ++i) 965 | sbuf[i] = alpha[msg[i]]; 966 | 967 | sbuf[n] = '\0'; 968 | 969 | logprintf("Print Message %04x %d (%d,%d) \"%s\"\n", ptr_to_word(msg).u16, n, x, y, sbuf); 970 | } 971 | 972 | // GAMECODE START 973 | 974 | // The entry point on reset or power up 975 | static void reset() 976 | { 977 | // xref 0000 978 | main_init(0); 979 | } 980 | 981 | // Executed via interrupt when the beam hits the middle of the screen 982 | static void midscreen_int() 983 | { 984 | // xref 0008 985 | // xref 008c 986 | m.vblankStatus = BEAM_MIDDLE; 987 | 988 | if (m.gameTasksRunning == 0) return; 989 | if (!m.gameMode && !(m.isrSplashTask & 0x1)) return; 990 | 991 | // xref 00a5 992 | // Run game objects but skip over first entry (player) 993 | 994 | RunGameObjs(u16_to_ptr(PLAYER_SHOT_ADDR)); 995 | CursorNextAlien(); 996 | } 997 | 998 | // Executed via interrupt when the beam hits the end of the screen 999 | static void vblank_int() 1000 | { 1001 | // xref 0010 1002 | m.vblankStatus = BEAM_VBLANK; 1003 | m.isrDelay--; 1004 | CheckHandleTilt(); 1005 | vblank_coins(); 1006 | 1007 | if (m.gameTasksRunning == 0) 1008 | return; 1009 | 1010 | if (m.gameMode) 1011 | { 1012 | // xref 006f 1013 | TimeFleetSound(); 1014 | m.shotSync = m.rolShotHeader.TimerExtra; 1015 | DrawAlien(); 1016 | RunGameObjs(u16_to_ptr(PLAYER_ADDR)); 1017 | TimeToSaucer(); 1018 | return; 1019 | } 1020 | 1021 | if (m.numCoins != 0) 1022 | { 1023 | // xref 005d 1024 | if (m.waitStartLoop) return; 1025 | 1026 | m.waitStartLoop = 1; 1027 | yield(YIELD_WAIT_FOR_START); 1028 | assert(FALSE); 1029 | } 1030 | 1031 | ISRSplTasks(); 1032 | } 1033 | 1034 | // Read coin input and handle debouncing 1035 | static void vblank_coins() 1036 | { 1037 | // xref 0020 1038 | 1039 | if (read_port(1) & COIN_BIT) 1040 | { 1041 | // xref 0067 1042 | m.coinSwitch = 1; // Remember switch state for debounce 1043 | 1044 | // xref 003f 1045 | // useless update 1046 | m.coinSwitch = 1; 1047 | return; 1048 | } 1049 | 1050 | // xref 0026 1051 | // Skip registering the credit if prev and current states were both zero 1052 | if (m.coinSwitch == 0) 1053 | return; 1054 | 1055 | uint8_t bcd = m.numCoins; 1056 | 1057 | // Add credit if it won't cause a rollover 1058 | if (bcd != 0x99) 1059 | { 1060 | uint8_t carry = 0; 1061 | m.numCoins = bcd_add(bcd, 1, &carry); 1062 | DrawNumCredits(); 1063 | } 1064 | 1065 | m.coinSwitch = 0; 1066 | } 1067 | 1068 | // Initialize the alien rack speed and position using the current player's alien rack data 1069 | static void InitRack() 1070 | { 1071 | // xref 00b1 1072 | 1073 | uint8_t* al_ref_ptr = GetAlRefPtr(); 1074 | 1075 | Word refpos; 1076 | refpos.y = *(al_ref_ptr); 1077 | refpos.x = *(al_ref_ptr + 1); 1078 | 1079 | m.refAlienPos = refpos; 1080 | m.alienPos = refpos; 1081 | 1082 | uint8_t dxr = *(al_ref_ptr - 1); 1083 | 1084 | if (dxr == 3) 1085 | --dxr; 1086 | 1087 | m.refAlienDelta.x = dxr; 1088 | m.rackDirection = (dxr == (uint8_t) -2) ? 1 : 0; 1089 | } 1090 | 1091 | // Set up P1 and P2 Rack data 1092 | static void InitAlienRacks() 1093 | { 1094 | // xref 00d7 1095 | 1096 | // Added for si78c testing infrastructure 1097 | // In normal operation, dx is hardcoded to 2 1098 | uint8_t dx = ((uint8_t*) &m)[0x00d8]; 1099 | 1100 | m.p1RefAlienDX = dx; 1101 | m.p2RefAlienDX = dx; 1102 | 1103 | // xref 08e4 1104 | if (m.twoPlayers) 1105 | return; 1106 | 1107 | ClearSmallSprite(xytosc(168, 224), 32, 0); 1108 | } 1109 | 1110 | // Draw the alien, called via vblank_int() 1111 | // Only one alien is ever drawn per frame, causing a ripple effect. 1112 | // This also somewhat determines the game speed. 1113 | // 1114 | // The alien rack is effectively moving at 1115 | // (2.0 / num_aliens) pixels per frame 1116 | // 1117 | // If 55 aliens are alive, then it will take almost one second to move all the aliens by 2 pixels. 1118 | // If 1 alien is alive, then it will only take one frame to move 2 pixels. 1119 | static void DrawAlien() 1120 | { 1121 | // xref 0100 1122 | 1123 | if (m.alienIsExploding) 1124 | { 1125 | if (--m.expAlienTimer) return; 1126 | 1127 | EraseSimpleSprite(m.expAlien.pos, 16); 1128 | 1129 | m.plyrShotStatus = 4; 1130 | m.alienIsExploding = 0; 1131 | SoundBits3Off(0xf7); 1132 | return; 1133 | } 1134 | 1135 | uint8_t ai = m.alienCurIndex; 1136 | uint8_t* alienptr = u16_to_ptr(m.playerDataMSB << 8 | ai); 1137 | 1138 | if (*alienptr == 0) 1139 | { 1140 | // The alien is dead, so skip it and tell CursorNextAlien to advance. 1141 | m.waitOnDraw = 0; 1142 | return; 1143 | } 1144 | 1145 | // Look up the correct alien sprite based on row and anim frame 1146 | 1147 | uint8_t row = m.alienRow; 1148 | static const uint8_t mul[] = {0,0,16,16,32}; 1149 | uint8_t* sprite = u16_to_ptr(0x1c00 + mul[row] + m.alienFrame * 48); 1150 | 1151 | SprDesc desc; 1152 | desc.pos = m.alienPos; 1153 | desc.spr = ptr_to_word(sprite); 1154 | desc.n = 16; 1155 | 1156 | DrawSprite(&desc); 1157 | m.waitOnDraw = 0; // This flag is synced with CursorNextAlien 1158 | } 1159 | 1160 | // Find the next live alien to draw. Also detects whether rack has reached the bottom. 1161 | static void CursorNextAlien() 1162 | { 1163 | // xref 0141 1164 | 1165 | if (m.playerOK == 0) return; 1166 | if (m.waitOnDraw != 0) return; 1167 | 1168 | Word ai; 1169 | ai.h = m.playerDataMSB; 1170 | ai.l = m.alienCurIndex; 1171 | uint8_t movecnt = 0x02; // limits MoveRefAlien to be called once 1172 | 1173 | // Advance the cursor until we find a live alien to draw. 1174 | // If the cursor reaches the end, move ref alien, flip anim and reset cursor to zero. 1175 | while (TRUE) 1176 | { 1177 | timeslice(); 1178 | ++ai.l; 1179 | 1180 | // inlined MoveRefAlien 1181 | if (ai.l == 55) 1182 | { 1183 | if (--movecnt == 0) return; 1184 | 1185 | m.alienCurIndex = 0; 1186 | ai.l = 0; 1187 | 1188 | uint8_t dy = m.refAlienDelta.y; 1189 | m.refAlienDelta.y = 0; 1190 | AddDelta(&m.refAlienDelta.y, dy); 1191 | 1192 | m.alienFrame = !m.alienFrame; 1193 | (void) (uint8_t) (m.playerDataMSB); // unused 1194 | } 1195 | 1196 | // If we found live alien to draw, then break 1197 | if (*word_to_ptr(ai)) break; 1198 | } 1199 | 1200 | m.alienCurIndex = ai.l; 1201 | 1202 | uint8_t row = 0; 1203 | Word pixnum = GetAlienCoords(&row, ai.l); 1204 | m.alienPos = pixnum; 1205 | 1206 | if (pixnum.y < 40) 1207 | { 1208 | // xref 1971 1209 | // kill the player due to invasion 1210 | 1211 | m.invaded = 1; 1212 | yield(YIELD_INVADED); 1213 | assert(FALSE); 1214 | } 1215 | 1216 | m.alienRow = row; 1217 | m.waitOnDraw = 1; 1218 | } 1219 | 1220 | // Given alien index k, return alien position and row 1221 | static Word GetAlienCoords(uint8_t *rowout, uint8_t k) 1222 | { 1223 | // xref 017a 1224 | 1225 | uint8_t row = k / 11; 1226 | uint8_t col = k % 11; 1227 | 1228 | uint8_t y = m.refAlienPos.y + 16 * row; 1229 | uint8_t x = m.refAlienPos.x + 16 * col; 1230 | 1231 | *rowout = row; 1232 | return u8_u8_to_word(x, y); 1233 | } 1234 | 1235 | // Init all the aliens at dest to alive 1236 | static void InitAliensSub(uint8_t* dest) 1237 | { 1238 | // xref 01c0 1239 | for (int i = 0; i < 55; ++i) 1240 | *dest++ = 1; 1241 | } 1242 | 1243 | // Init all the aliens for P1 1244 | static void InitAliensP1() 1245 | { 1246 | // xref 01c0 1247 | InitAliensSub(u16_to_ptr(P1_ADDR)); 1248 | } 1249 | 1250 | // Draw a one pixel (leftmost/bottommost in byte) stripe across the screen (224 pix wide) 1251 | // 16 pixels above the origin. 1252 | static void DrawBottomLine() 1253 | { 1254 | // xref 01cf 1255 | ClearSmallSprite(xytosc(0,16), 224, 1); 1256 | } 1257 | 1258 | // Given four bytes at vecptr treat them as two vecs - dy dx, y x 1259 | // and do this: 1260 | // 1261 | // (y,x) += (c, dx) 1262 | // 1263 | // Used to move objects 1264 | static uint8_t AddDelta(uint8_t* vecptr, uint8_t c) 1265 | { 1266 | vecptr++; // skip dy 1267 | 1268 | uint8_t dy = c; 1269 | uint8_t dx = *vecptr++; 1270 | 1271 | uint8_t y = *vecptr + dy; 1272 | *vecptr++ = y; 1273 | 1274 | uint8_t x = *vecptr + dx; 1275 | *vecptr++ = x; 1276 | 1277 | return x; 1278 | } 1279 | 1280 | // Restore into RAM mirror (addr, n) from ROM 1281 | static void RestoreFromROM(uint8_t* addr, size_t n) 1282 | { 1283 | BlockCopy(addr, rompos(addr), n); 1284 | } 1285 | 1286 | // Restore the entire RAM mirror (256 bytes), used at startup 1287 | static void CopyRomtoRam() 1288 | { 1289 | // xref 01e6 1290 | RestoreFromROM(u16_to_ptr(0x2000), 256); 1291 | } 1292 | 1293 | // Partially restore the RAM mirror (first 192 bytes) 1294 | // The last 64 bytes are managed elsewhere, and some are 1295 | // persistent across games (hi scores etc) 1296 | static void CopyRAMMirror() 1297 | { 1298 | // xref 01e4 1299 | RestoreFromROM(u16_to_ptr(0x2000), 192); 1300 | } 1301 | 1302 | // Initialize P1 shields into off screen buffer 1303 | static void DrawShieldP1() 1304 | { 1305 | // xref 01ef 1306 | DrawShieldCommon(m.p1ShieldBuffer); 1307 | } 1308 | 1309 | // Initialize P2 shields into off screen buffer 1310 | static void DrawShieldP2() 1311 | { 1312 | // xref 01f5 1313 | DrawShieldCommon(m.p2ShieldBuffer); 1314 | } 1315 | 1316 | // Initialize shields into given buffer 1317 | static void DrawShieldCommon(uint8_t* dest) 1318 | { 1319 | // xref 01f8 1320 | size_t n = 44; 1321 | 1322 | for (int i = 0; i < 4; ++i, dest += n) 1323 | BlockCopy(dest, m.SHIELD_SPRITE, n); 1324 | } 1325 | 1326 | // Copy on screen shields into P1 off screen buffer to remember damage 1327 | static void RememberShieldsP1() 1328 | { 1329 | // xref 0209 1330 | CopyShields(1, m.p1ShieldBuffer); 1331 | } 1332 | 1333 | // Copy on screen shields into P2 off screen buffer to remember damage 1334 | static void RememberShieldsP2() 1335 | { 1336 | // xref 020e 1337 | CopyShields(1, m.p2ShieldBuffer); 1338 | } 1339 | 1340 | // Copy off screen shields from P2 buffer back to screen 1341 | static void RestoreShieldsP2() 1342 | { 1343 | // xref 0213 1344 | CopyShields(0, m.p2ShieldBuffer); 1345 | } 1346 | 1347 | // Copy off screen shields from P1 buffer back to screen 1348 | static void RestoreShieldsP1() 1349 | { 1350 | // xref 021a 1351 | CopyShields(0, m.p1ShieldBuffer); 1352 | } 1353 | 1354 | // Generic shield copy routine. Copy into or out of given buffer. 1355 | // dir = 0 - buffer to screen 1356 | // dir = 1 - screen to buffer 1357 | static void CopyShields(uint8_t dir, uint8_t* sprbuf) 1358 | { 1359 | // xref 021e 1360 | m.tmp2081 = dir; 1361 | 1362 | Word sprsz = u8_u8_to_word(22, 2); // 22 rows, 2 bytes per row 1363 | 1364 | ShieldBufferCursor cursor; 1365 | cursor.sc = xytosc(32, 48); 1366 | cursor.iter = sprbuf; 1367 | 1368 | for (int i = 0; i < 4; ++i) 1369 | { 1370 | uint8_t unused = m.tmp2081; 1371 | (void) (unused); 1372 | 1373 | CopyShieldBuffer(&cursor, sprsz, dir); 1374 | 1375 | cursor.sc.u16 += xysc(23, 0); 1376 | } 1377 | } 1378 | 1379 | // Do the generic (base class) handling for the 5 game objects. 1380 | // Checks the timer flags to see if object is ready to run, and if so 1381 | // finds the appropriate subclass handler and calls it. 1382 | static void RunGameObjs(uint8_t* ptr) 1383 | { 1384 | // xref 0248 1385 | 1386 | for ( ; ; ptr += 16) 1387 | { 1388 | GameObjHeader* obj = (GameObjHeader*)(ptr); 1389 | uint8_t timer_hi = obj->TimerMSB; 1390 | 1391 | // end of list check 1392 | if (timer_hi == 0xff) return; 1393 | 1394 | // object skip check 1395 | if (timer_hi == 0xfe) continue; 1396 | 1397 | uint8_t timer_lo = obj->TimerLSB; 1398 | 1399 | // decrement timer if its not zero 1400 | if (timer_hi | timer_lo) 1401 | { 1402 | // xref 0277 1403 | // timers are big endian for some reason. 1404 | 1405 | if (timer_lo == 0) --timer_hi; // decrement msb if necc 1406 | --timer_lo; // decrement lsb 1407 | 1408 | obj->TimerLSB = timer_lo; 1409 | obj->TimerMSB = timer_hi; 1410 | 1411 | continue; 1412 | } 1413 | 1414 | uint8_t timer_extra = obj->TimerExtra; 1415 | 1416 | if (timer_extra != 0) 1417 | { 1418 | // xref 0288 1419 | obj->TimerExtra--; 1420 | continue; 1421 | } 1422 | 1423 | // The object is ready to run, so grab the handler and run it 1424 | 1425 | uint16_t fnlo = obj->Handler.l; 1426 | uint16_t fnhi = obj->Handler.h; 1427 | uint16_t fn = fnhi << 8 | fnlo; 1428 | 1429 | uint8_t* data = (ptr + sizeof(GameObjHeader)); 1430 | 1431 | switch(fn) 1432 | { 1433 | case 0x028e: GameObj0(data); break; 1434 | case 0x03bb: GameObj1(data); break; 1435 | case 0x0476: GameObj2(data); break; 1436 | case 0x04b6: GameObj3(data); break; 1437 | case 0x0682: GameObj4(data); break; 1438 | case 0x050e: ProcessSquigglyShot(); break; // splash anim 1439 | default: 1440 | assert(FALSE); 1441 | break; 1442 | } 1443 | } 1444 | } 1445 | 1446 | // Handles player movement and rendering 1447 | // Unlike other game objects, this is only run during the vblank interrupt, and 1448 | // runs on each vblank interrupt 1449 | static void GameObj0(uint8_t* unused) 1450 | { 1451 | // xref 028e 1452 | uint8_t pstate = m.playerAlive; 1453 | 1454 | if (pstate != 0xff) // 0xff means player is alive 1455 | { 1456 | HandleBlowingUpPlayer(pstate); 1457 | return; 1458 | } 1459 | 1460 | // xref 033b 1461 | m.playerOK = 1; 1462 | 1463 | // xref 03b0 1464 | if (m.enableAlienFire == 0) 1465 | { 1466 | // xref 03b3 1467 | 1468 | if (--m.alienFireDelay == 0) 1469 | m.enableAlienFire = 1; 1470 | } 1471 | 1472 | // xref 034a 1473 | uint8_t x = m.playerDesc.pos.x; 1474 | uint8_t input = 0; 1475 | 1476 | if (m.gameMode == 0) 1477 | { 1478 | input = m.nextDemoCmd; 1479 | } 1480 | else 1481 | { 1482 | input = ReadInputs(); 1483 | 1484 | // Map joystick controls into the same domain as the demo commands 1485 | if ((input >> 6) & 0x1) 1486 | input = 1; 1487 | else if ((input >> 5) & 0x1) 1488 | input = 2; 1489 | else 1490 | input = 0; 1491 | } 1492 | 1493 | if (input == 0) 1494 | { 1495 | // Do nothing 1496 | } 1497 | else if (input == 1) 1498 | { 1499 | // Move player to the right 1500 | if (x != xpix(185)) 1501 | { 1502 | ++x; 1503 | m.playerDesc.pos.x = x; 1504 | } 1505 | } 1506 | else if (input == 2) 1507 | { 1508 | // Move player to the left 1509 | if (x != xpix(16)) 1510 | { 1511 | --x; 1512 | m.playerDesc.pos.x = x; 1513 | } 1514 | } 1515 | else 1516 | { 1517 | assert(FALSE); 1518 | } 1519 | 1520 | DrawPlayer(); 1521 | } 1522 | 1523 | // Handle the player's death animation. Resets the stack once complete, 1524 | // and re-entry is through player_death(0) 1525 | static void HandleBlowingUpPlayer(uint8_t anim) 1526 | { 1527 | // xref 0296 1528 | if (--m.expAnimateTimer != 0) 1529 | return; 1530 | 1531 | m.playerOK = 0; 1532 | m.enableAlienFire = 0; 1533 | m.alienFireDelay = 48; 1534 | m.expAnimateTimer = 5; 1535 | 1536 | if (--m.expAnimateCnt != 0) 1537 | { 1538 | // Still animating the explosion 1539 | anim = !anim; 1540 | m.playerAlive = anim; 1541 | 1542 | m.playerDesc.spr = ptr_to_word((m.PLAYER_SPRITES + (anim+1) * 16)); 1543 | DrawPlayer(); 1544 | return; 1545 | } 1546 | 1547 | EraseSimpleSprite(m.playerDesc.pos, 16); 1548 | RestoreFromROM(u16_to_ptr(PLAYER_ADDR), PLAYER_SIZE); 1549 | 1550 | SoundBits3Off(0); 1551 | 1552 | if (m.invaded) 1553 | return; 1554 | 1555 | // Return to splash screens in demo mode 1556 | if (m.gameMode == 0) 1557 | return; 1558 | 1559 | yield(YIELD_PLAYER_DEATH); 1560 | assert(FALSE); 1561 | } 1562 | 1563 | // Handle cleanup tasks related to player dying, such as lives and score adjustment, 1564 | // switching players and calling GameOver if necessary. 1565 | // If invaded is one, the player loses the game, regardless of number of lives left. 1566 | // This routine is called after a stack reset, see on_invaded() and HandleBlowingUpPlayer() 1567 | static void player_death(int invaded) 1568 | { 1569 | // xref 02d4 1570 | int switch_players = player_death_sub(invaded); 1571 | 1572 | if (!switch_players) 1573 | { 1574 | if (!*(GetOtherPlayerAliveFlag()) || !m.twoPlayers) 1575 | { 1576 | RemoveShip(); 1577 | NewGame(0, 0, INIT | PROMPT | SHIELDS); 1578 | } 1579 | } 1580 | 1581 | uint8_t pnum = m.playerDataMSB; 1582 | 1583 | if (pnum & 0x1) 1584 | RememberShieldsP1(); 1585 | else 1586 | RememberShieldsP2(); 1587 | 1588 | uint8_t adx = 0; 1589 | Word apos; 1590 | uint8_t* aref = GetRefAlienInfo(&adx, &apos); 1591 | 1592 | *aref = apos.y; 1593 | *(aref+1) = apos.x; 1594 | 1595 | *(aref-1) = adx; 1596 | 1597 | // about to switch players. 1598 | CopyRAMMirror(); 1599 | 1600 | uint8_t carry = pnum & 0x1; 1601 | uint8_t pmsb = 0x21; // p1 1602 | uint8_t cbit = 0; 1603 | 1604 | if (carry) 1605 | { 1606 | cbit = 0x20; // cocktail bit=1 1607 | pmsb = 0x22; 1608 | } 1609 | 1610 | m.playerDataMSB = pmsb; // change players 1611 | TwoSecDelay(); 1612 | 1613 | m.playerHeader.TimerLSB = 0; // clear player object timer 1614 | write_port(5, cbit); 1615 | 1616 | m.soundPort5 = (cbit + 1); 1617 | ClearPlayField(); 1618 | RemoveShip(); 1619 | 1620 | // jmp to 079b. (newgame + skip) 1621 | NewGame(0, 0, INIT); 1622 | } 1623 | 1624 | // Player death cleanup subroutine. Game is lost immediately if invaded=1 1625 | // Returns true if caller should switch players, false otherwise 1626 | // This subroutine will not return if all players have lost the game, instead it 1627 | // will call through to GameOver, which will start a new game 1628 | static int player_death_sub(int invaded) 1629 | { 1630 | if (!invaded) 1631 | { 1632 | DsableGameTasks(); 1633 | 1634 | uint8_t* unused; 1635 | 1636 | // still got some ships, keep going 1637 | if (GetNumberOfShips(&unused) != 0) 1638 | return FALSE; 1639 | 1640 | PrintNumShips(0); 1641 | } 1642 | 1643 | // handle losing the game 1644 | 1645 | *CurPlyAlive() = 0; 1646 | 1647 | uint8_t* sc = GetScoreDescriptor(); 1648 | sc++; 1649 | 1650 | uint8_t* hi = &m.HiScor.h; 1651 | 1652 | uint8_t hi_score_msb = *(hi); 1653 | uint8_t pl_score_msb = *(sc); 1654 | 1655 | hi--; 1656 | sc--; 1657 | 1658 | uint8_t hi_score_lsb = *(hi); 1659 | uint8_t pl_score_lsb = 0; 1660 | 1661 | int higher = FALSE; 1662 | 1663 | if (hi_score_msb == pl_score_msb) 1664 | { 1665 | // same msb, must check lower 1666 | pl_score_lsb = *(sc); 1667 | higher = pl_score_lsb > hi_score_lsb; 1668 | } 1669 | else 1670 | { 1671 | higher = pl_score_msb > hi_score_msb; 1672 | } 1673 | 1674 | if (higher) 1675 | { 1676 | *hi++ = *sc++; 1677 | *hi = *sc; 1678 | PrintHiScore(); 1679 | } 1680 | 1681 | // xref 1698 1682 | if (m.twoPlayers) 1683 | { 1684 | // Game over player 1685 | Word sc = xytosc(32, 24); 1686 | sc = PrintMessageDel(sc, m.MSG_GAME_OVER__PLAYER___, 0x14); 1687 | 1688 | sc.u16 -= xysc(16, 0); // back up to player indicator 1689 | uint8_t b = (m.playerDataMSB & 0x1) ? 0x1b : 0x1c; 1690 | 1691 | sc = DrawChar(sc, b); // print player num 1692 | OneSecDelay(); 1693 | 1694 | if (*(GetOtherPlayerAliveFlag()) == 0) 1695 | { 1696 | GameOver(); // won't return. 1697 | assert(FALSE); 1698 | } 1699 | 1700 | // switch players 1701 | return TRUE; 1702 | } 1703 | else 1704 | { 1705 | GameOver(); // won't return 1706 | assert(FALSE); 1707 | } 1708 | 1709 | return FALSE; 1710 | } 1711 | 1712 | // Generic player shot drawing routine, originally multiple small fragments 1713 | static void DrawPlayerShot(int op) 1714 | { 1715 | // xref 0404 1716 | // xref 03f4 1717 | SprDesc plyshot = ReadPlyShot(); 1718 | 1719 | if (op == OP_BLEND) 1720 | DrawShiftedSprite(&plyshot); 1721 | else if (op == OP_ERASE) 1722 | EraseShifted(&plyshot); 1723 | else 1724 | assert(FALSE); 1725 | } 1726 | 1727 | // Handles player bullet movement, collision detection and rendering. 1728 | // At the end of the routine, the player's shot count is used to 1729 | // set up the next saucer direction and score. 1730 | static void GameObj1(uint8_t* unused) 1731 | { 1732 | // xref 03bb 1733 | // 1734 | // Shot states: 1735 | // 1736 | // :Available(0), :Initiated(1), :Moving(2), :HitNotAlien(3), 1737 | // :AlienExploded(4), :AlienExploding(5) 1738 | 1739 | if (!CompXrToBeam(&m.playerShotDesc.pos.x)) 1740 | return; 1741 | 1742 | uint8_t status = m.plyrShotStatus; 1743 | 1744 | if (status == 0) 1745 | return; 1746 | 1747 | if (status == 1) 1748 | { 1749 | // xref 03fa InitPlyShot 1750 | m.plyrShotStatus = 2; 1751 | m.playerShotDesc.pos.x = m.playerDesc.pos.x + 8; 1752 | DrawPlayerShot(OP_BLEND); 1753 | return; 1754 | } 1755 | else if (status == 2) 1756 | { 1757 | // xref 040a MovePlyShot 1758 | SprDesc copy = ReadPlyShot(); 1759 | EraseShifted(©); 1760 | 1761 | copy.pos.y += m.shotDeltaYr; 1762 | m.playerShotDesc.pos.y = copy.pos.y; 1763 | 1764 | DrawSprCollision(©); 1765 | uint8_t collided = m.collision; 1766 | 1767 | if (!collided) return; 1768 | 1769 | m.alienIsExploding = collided; 1770 | return; 1771 | } 1772 | 1773 | if (status != 3) 1774 | { 1775 | // xref 042a 1776 | if (status == 5) return; 1777 | 1778 | // continues at EndOfBlowup 1779 | } 1780 | else 1781 | { 1782 | // xref 03d7 1783 | if (--m.blowUpTimer != 0) 1784 | { 1785 | // The shot is blowing up 1786 | if (m.blowUpTimer != 0x0f) return; 1787 | 1788 | // Draw the explosion the first time through 1789 | // xref 03df 1790 | DrawPlayerShot(OP_ERASE); 1791 | 1792 | // Change the shot sprite to the explosion. 1793 | m.playerShotDesc.spr.l++; 1794 | 1795 | // Modify the coords slightly for the explosion 1796 | m.playerShotDesc.pos.y--; // y -= 2 1797 | m.playerShotDesc.pos.y--; 1798 | 1799 | m.playerShotDesc.pos.x--; // x -= 3 1800 | m.playerShotDesc.pos.x--; 1801 | m.playerShotDesc.pos.x--; 1802 | 1803 | m.playerShotDesc.n = 8; 1804 | DrawPlayerShot(OP_BLEND); 1805 | return; 1806 | } 1807 | } 1808 | 1809 | // xref 0436 EndOfBlowup 1810 | DrawPlayerShot(OP_ERASE); 1811 | 1812 | // xref 0444 1813 | // reset the shot 1814 | RestoreFromROM(u16_to_ptr(PLAYER_SHOT_DATA_ADDR), PLAYER_SHOT_DATA_SIZE); 1815 | 1816 | // The remaining code in GameObj0 is to do with adjusting the saucer bonus and 1817 | // direction, which is changed up on every player shot fired. 1818 | 1819 | // Adjust the saucer bonus. 1820 | { 1821 | Word table = m.sauScore; 1822 | 1823 | table.l++; 1824 | 1825 | if (table.l >= 0x63) 1826 | table.l = 0x54; 1827 | 1828 | m.sauScore = table; 1829 | } 1830 | 1831 | Word shots = m.shotCount; 1832 | 1833 | shots.l++; 1834 | m.shotCount = shots; 1835 | 1836 | // xref 0461 1837 | // If saucer still on screen, don't reset the direction. 1838 | if (m.saucerActive) 1839 | return; 1840 | 1841 | // xref 0462 1842 | // 1843 | // This code is using the shot counter as an index into the ROM 1844 | // (where some code resides), for the purposes of random number generation. 1845 | // 1846 | // For the saucer direction logic, only bit 0 of each bytecode is used. 1847 | // 1848 | // The 256 bytes used reside at 0800 -> 08FF 1849 | // 1850 | // If you check bit 0 of each byte in that ROM section, you will find that there is no bias, 1851 | // and there are exactly 128 0's and 128 1's. 1852 | // 1853 | // It seems unlikely that this was an accident, I think Nishikado deliberately constructed 1854 | // the ROM this way, and used some well placed NOPs to achieve fair balance. 1855 | // 1856 | // E.g. these NOPs 1857 | // 1858 | // 0854: 00 00 00 1859 | // 0883: 00 00 00 1860 | 1861 | // This information can be exploited to the player's advantage. 1862 | // 1863 | // If using the shot counting trick to get high scores, the 1864 | // expected saucer direction for the first 6 saucers (if counting), 1865 | // will be as follows: 1866 | // 1867 | // [22,37,52,67,82,97] 1868 | // [0, 1, 1, 0, 1, 1] 1869 | // [L, R, R, L, R, R] 1870 | 1871 | uint8_t v = *(word_to_ptr(shots)); 1872 | 1873 | // if lo bit of res is 0, then delta = -2, x = 192 (moving left from rhs) 1874 | // if lo bit of res is 1, then delta = +2, x = 9 (moving right from lhs) 1875 | 1876 | uint8_t delta = -2; 1877 | uint8_t x = xpix(192); 1878 | 1879 | if (v & 0x1) 1880 | { 1881 | delta = 2; 1882 | x = xpix(9); 1883 | } 1884 | 1885 | m.saucerDesc.pos.x = x; 1886 | m.saucerDXr = delta; 1887 | } 1888 | 1889 | // Return a copy of the player shot sprite descriptor 1890 | static SprDesc ReadPlyShot() 1891 | { 1892 | // xref 0430 1893 | return ReadDesc(&m.playerShotDesc); 1894 | } 1895 | 1896 | // Handles alien rolling shot firing, movement, collision detection and rendering. 1897 | // This is the shot that specifically targets the player. 1898 | // Most of the logic is shared between the 3 types inside HandleAlienShot. 1899 | static void GameObj2(uint8_t* unused1) 1900 | { 1901 | // xref 0476 1902 | RestoreFromROM(&m.rolShotHeader.TimerExtra, 1); 1903 | 1904 | if (m.rolShotData.CFir.u16 == 0) 1905 | { 1906 | // The rolling shot doesn't use a firing table to choose a column to fire 1907 | // from, because it specifically targets the player. 1908 | // 1909 | // It just uses this member as a flag to delay firing the first rolling shot 1910 | m.rolShotData.CFir.u16 = 0xffff; 1911 | return; 1912 | } 1913 | 1914 | ToShotStruct(&m.rolShotData, ROL_SHOT_PICEND); 1915 | 1916 | m.otherShot1 = m.pluShotData.StepCnt; 1917 | m.otherShot2 = m.squShotData.StepCnt; 1918 | 1919 | HandleAlienShot(); 1920 | 1921 | if (m.aShot.BlowCnt != 0) 1922 | { 1923 | // shot still running, copy updated data from active -> rolling and return. 1924 | FromShotStruct(&m.rolShotData); 1925 | return; 1926 | } 1927 | 1928 | RestoreFromROM(u16_to_ptr(ROLLING_SHOT_ADDR), ROLLING_SHOT_SIZE); 1929 | } 1930 | 1931 | // Handles alien plunger shot firing, movement, collision detection and rendering. 1932 | static void GameObj3(uint8_t* unused) 1933 | { 1934 | // xref 04b6 1935 | if (m.skipPlunger) return; 1936 | if (m.shotSync != 1) return; 1937 | 1938 | ToShotStruct(&m.pluShotData, PLU_SHOT_PICEND); 1939 | 1940 | m.otherShot1 = m.rolShotData.StepCnt; 1941 | m.otherShot2 = m.squShotData.StepCnt; 1942 | 1943 | HandleAlienShot(); 1944 | 1945 | if (m.aShot.CFir.l >= 16) 1946 | { 1947 | m.aShot.CFir.l = *(rompos(&m.pluShotData.CFir.l)); 1948 | } 1949 | 1950 | if (m.aShot.BlowCnt) 1951 | { 1952 | FromShotStruct(&m.pluShotData); 1953 | return; 1954 | } 1955 | 1956 | RestoreFromROM(u16_to_ptr(PLUNGER_SHOT_ADDR), PLUNGER_SHOT_SIZE); 1957 | 1958 | if (m.numAliens == 1) 1959 | m.skipPlunger = 1; 1960 | 1961 | m.pluShotData.CFir = m.aShot.CFir; 1962 | } 1963 | 1964 | // Handles alien squiggly shot firing, movement, collision detection and rendering. 1965 | // This is very similar logic to the plunger shot except the column firing table 1966 | // is different. 1967 | static void ProcessSquigglyShot() 1968 | { 1969 | // xref 050f 1970 | ToShotStruct(&m.squShotData, SQU_SHOT_PICEND); 1971 | 1972 | m.otherShot1 = m.pluShotData.StepCnt; 1973 | m.otherShot2 = m.rolShotData.StepCnt; 1974 | 1975 | HandleAlienShot(); 1976 | 1977 | if (m.aShot.CFir.l >= 21) 1978 | { 1979 | // Restores to the rom lsb values of '6' 1980 | m.aShot.CFir.l = *(rompos(&m.squShotData.CFir.l)); 1981 | } 1982 | 1983 | if (m.aShot.BlowCnt) 1984 | { 1985 | FromShotStruct(&m.squShotData); 1986 | return; 1987 | } 1988 | 1989 | RestoreFromROM(u16_to_ptr(SQUIGGLY_SHOT_ADDR), SQUIGGLY_SHOT_SIZE); 1990 | m.squShotData.CFir = m.aShot.CFir; 1991 | } 1992 | 1993 | // Copy an alien shot structure from src into the active alien shot structure, 1994 | // and configure the shot animation. 1995 | static void ToShotStruct(AShot* src, uint8_t picend) 1996 | { 1997 | // xref 0550 1998 | m.shotPicEnd = picend; 1999 | BlockCopy(&m.aShot, src, 11); 2000 | } 2001 | 2002 | // Copy the active alien shot structure into dest 2003 | static void FromShotStruct(AShot* dest) 2004 | { 2005 | // xref 055b 2006 | BlockCopy( dest, &m.aShot, 11); 2007 | } 2008 | 2009 | // This logic is shared between the 3 shot types. 2010 | // Handles shot firing, movement, collision detection and rendering. 2011 | static void HandleAlienShot() 2012 | { 2013 | // xref 0563 2014 | if ((m.aShot.Status & SHOT_ACTIVE) != 0) 2015 | { 2016 | HandleAlienShotMove(); 2017 | return; 2018 | } 2019 | 2020 | uint8_t shooting_c = (m.isrSplashTask == 0x04); 2021 | uint8_t fire_enabled = m.enableAlienFire; 2022 | 2023 | if (shooting_c) 2024 | { 2025 | // Special case for the splash animation 2026 | m.aShot.Status |= SHOT_ACTIVE; 2027 | m.aShot.StepCnt++; 2028 | 2029 | return; 2030 | } 2031 | 2032 | if (!fire_enabled) 2033 | return; 2034 | 2035 | m.aShot.StepCnt = 0; 2036 | 2037 | { 2038 | uint8_t steps = m.otherShot1; 2039 | if (steps && steps <= m.aShotReloadRate) return; 2040 | } 2041 | 2042 | { 2043 | uint8_t steps = m.otherShot2; 2044 | if (steps && steps <= m.aShotReloadRate) return; 2045 | } 2046 | 2047 | uint8_t col = 0; 2048 | 2049 | if (m.aShot.Track == 0) 2050 | { 2051 | // xref 061b 2052 | // Make a tracking shot, by finding the column that is above the player 2053 | 2054 | Word res = FindColumn(m.playerDesc.pos.x + 8); // find column over centre of player 2055 | col = res.h; // res.l unused 2056 | 2057 | if (col >= 12) 2058 | col = 11; 2059 | } 2060 | else 2061 | { 2062 | // xref 059c 2063 | // Use the firing table pointer to pick the column, and advance it 2064 | uint8_t* hl = word_to_ptr(m.aShot.CFir); 2065 | col = *hl++; 2066 | m.aShot.CFir = ptr_to_word(hl); 2067 | } 2068 | 2069 | // xref 05a5 2070 | uint8_t k = 0; 2071 | uint8_t found = FindInColumn(&k, col); 2072 | 2073 | if (!found) 2074 | return; 2075 | 2076 | uint8_t row_unused = 0; 2077 | Word pixnum = GetAlienCoords(&row_unused, k); 2078 | 2079 | pixnum.x += 7; 2080 | pixnum.y -= 10; 2081 | 2082 | m.aShot.Desc.pos = pixnum; 2083 | m.aShot.Status |= SHOT_ACTIVE; 2084 | m.aShot.StepCnt++; 2085 | 2086 | return; 2087 | } 2088 | 2089 | // Handle moving the alien shot and some collision detection response. 2090 | // Returns 1 if shot status needs to be set to blowing up, 0 if not. 2091 | static int DoHandleAlienShotMove() 2092 | { 2093 | if (!CompXrToBeam(&m.aShot.Desc.pos.x)) 2094 | return 0; 2095 | 2096 | if (m.aShot.Status & SHOT_BLOWUP) 2097 | { 2098 | ShotBlowingUp(); 2099 | return 0; 2100 | } 2101 | 2102 | // xref 05cf 2103 | m.aShot.StepCnt++; 2104 | EraseAlienShot(); 2105 | 2106 | // Animate the shot 2107 | uint8_t shotpic = m.aShot.Desc.spr.l + 3; 2108 | 2109 | if (shotpic > m.shotPicEnd) 2110 | shotpic -= 12; 2111 | 2112 | m.aShot.Desc.spr.l = shotpic; 2113 | m.aShot.Desc.pos.y = m.aShot.Desc.pos.y + m.alienShotDelta; 2114 | DrawAlienShot(); 2115 | 2116 | // xref 05f3 2117 | uint8_t y = m.aShot.Desc.pos.y; 2118 | 2119 | if (y < 21) 2120 | return 1; 2121 | 2122 | if (!m.collision) 2123 | return 0; 2124 | 2125 | y = m.aShot.Desc.pos.y; 2126 | 2127 | // below or above players area ? 2128 | if (y < 30 || y >= 39) 2129 | return 1; 2130 | 2131 | if (!is_godmode()) 2132 | m.playerAlive = 0; 2133 | 2134 | return 1; 2135 | } 2136 | 2137 | // Handle moving the alien shot and some collision detection response. 2138 | static void HandleAlienShotMove() 2139 | { 2140 | // xref 05c1 2141 | int exploded = DoHandleAlienShotMove(); 2142 | 2143 | if (exploded) 2144 | m.aShot.Status |= SHOT_BLOWUP; 2145 | } 2146 | 2147 | // Find a live alien in the given column. 2148 | static uint8_t FindInColumn(uint8_t *out, uint8_t col) 2149 | { 2150 | // xref 062f 2151 | Word hl; 2152 | 2153 | hl.h = m.playerDataMSB; 2154 | hl.l = col - 1; 2155 | 2156 | int found = 0; 2157 | 2158 | for (int i = 0; i < 5; ++i) 2159 | { 2160 | if (*word_to_ptr(hl)) 2161 | { 2162 | found = 1; 2163 | break; 2164 | } 2165 | 2166 | hl.l += 11; 2167 | } 2168 | 2169 | *out = hl.l; 2170 | return found; 2171 | } 2172 | 2173 | // Handle alien shot explosion animation 2174 | static void ShotBlowingUp() 2175 | { 2176 | // xref 0644 2177 | m.aShot.BlowCnt--; 2178 | 2179 | uint8_t blowcnt = m.aShot.BlowCnt; 2180 | 2181 | if (blowcnt == 3) 2182 | { 2183 | EraseAlienShot(); 2184 | m.aShot.Desc.spr = ptr_to_word(m.AlienShotExplodingSprite); 2185 | 2186 | // Offset the explision sprite from the shot by (-2,-2) 2187 | 2188 | m.aShot.Desc.pos.x--; 2189 | m.aShot.Desc.pos.x--; 2190 | 2191 | m.aShot.Desc.pos.y--; 2192 | m.aShot.Desc.pos.y--; 2193 | 2194 | m.aShot.Desc.n = 6; 2195 | 2196 | DrawAlienShot(); 2197 | return; 2198 | } 2199 | 2200 | if (blowcnt) 2201 | return; 2202 | 2203 | EraseAlienShot(); 2204 | } 2205 | 2206 | // Draw the active alien shot and do collision detection 2207 | static void DrawAlienShot() 2208 | { 2209 | // xref 066c 2210 | SprDesc desc = ReadDesc(&m.aShot.Desc); 2211 | DrawSprCollision(&desc); 2212 | return; 2213 | } 2214 | 2215 | // Erase the active alien shot 2216 | static void EraseAlienShot() 2217 | { 2218 | // xref 0675 2219 | SprDesc desc = ReadDesc(&m.aShot.Desc); 2220 | EraseShifted(&desc); 2221 | return; 2222 | } 2223 | 2224 | // Handles either the Squiggly shot or the Saucer, depending on the saucer timer. 2225 | // See ProcessSquigglyShot for squiggly shot logic. 2226 | // The bulk of this routine handles saucer movement, collision response, rendering 2227 | // and scoring. 2228 | static void GameObj4(uint8_t* unused) 2229 | { 2230 | // xref 0682 2231 | if (m.shotSync != 2) return; 2232 | 2233 | if (m.saucerStart == 0) 2234 | { 2235 | ProcessSquigglyShot(); 2236 | return; 2237 | } 2238 | 2239 | if (m.squShotData.StepCnt) 2240 | { 2241 | ProcessSquigglyShot(); 2242 | return; 2243 | } 2244 | 2245 | if (!m.saucerActive) 2246 | { 2247 | if (m.numAliens < 8) 2248 | { 2249 | ProcessSquigglyShot(); 2250 | return; 2251 | } 2252 | 2253 | m.saucerActive = 1; 2254 | DrawSaucer(); 2255 | } 2256 | 2257 | uint8_t carry = CompXrToBeam(&m.saucerDesc.pos.x); 2258 | 2259 | if (!carry) 2260 | return; 2261 | 2262 | if (!m.saucerHit) 2263 | { 2264 | uint8_t x = m.saucerDesc.pos.x; 2265 | uint8_t dx = m.saucerDXr; 2266 | 2267 | m.saucerDesc.pos.x = x + dx; 2268 | DrawSaucer(); 2269 | 2270 | x = m.saucerDesc.pos.x; 2271 | 2272 | // check edges 2273 | if (x < xpix(8)) 2274 | { 2275 | RemoveSaucer(); 2276 | return; 2277 | } 2278 | 2279 | if (x >= xpix(193)) 2280 | { 2281 | RemoveSaucer(); 2282 | return; 2283 | } 2284 | 2285 | return; 2286 | } 2287 | 2288 | SoundBits3Off(0xfe); // turn off saucer sound 2289 | 2290 | m.saucerHitTime--; 2291 | uint8_t timer = m.saucerHitTime; 2292 | 2293 | if (timer == 31) 2294 | { 2295 | // xref 074b 2296 | // Turn on the sound and draw the saucer explosion 2297 | uint8_t snd = m.soundPort5 | 16; 2298 | m.soundPort5 = snd; 2299 | SetSoundWithoutFleet(snd); 2300 | 2301 | m.saucerDesc.spr = ptr_to_word(m.SpriteSaucerExp); 2302 | DrawSaucer(); 2303 | 2304 | return; 2305 | } 2306 | 2307 | if (timer == 24) 2308 | { 2309 | // xref 070c 2310 | m.adjustScore = 1; 2311 | 2312 | // Get the score for the saucer which is set based on shots fired in GameObj0 2313 | uint8_t score = *(word_to_ptr(m.sauScore)); 2314 | 2315 | // Find the index of the score in the table 2316 | int i = 0; 2317 | for (i = 0; i < 4; ++i) 2318 | { 2319 | if (m.SauScrValueTab[i] == score) 2320 | break; 2321 | } 2322 | 2323 | // Use it to find the matching LSB for the score text, and set it in saucerDesc 2324 | m.saucerDesc.spr.l = m.SauScrStrTab[i]; 2325 | 2326 | // Multiply the score by 16 (i.e. bcd shift left one digit), to get 50,100,150,300 in BCD 2327 | m.scoreDelta.u16 = score * 16; 2328 | 2329 | // Print the bonus score message, using pointer set in saucerDesc.spr above 2330 | SprDesc desc = GetSaucerInfo(); 2331 | PrintMessage(desc.sc, word_to_ptr(desc.spr), 3); 2332 | 2333 | return; 2334 | } 2335 | 2336 | // xref 06e8 2337 | if (timer != 0) 2338 | return; 2339 | 2340 | uint8_t snd = m.soundPort5 & 0xef; 2341 | m.soundPort5 = snd; 2342 | write_port(5, snd & 0x20); 2343 | 2344 | RemoveSaucer(); 2345 | } 2346 | 2347 | // Saucer cleanup tasks 2348 | static void RemoveSaucer() 2349 | { 2350 | // xref 06f9 2351 | SprDesc desc = ReadDesc(&m.saucerDesc); 2352 | ClearSmallSprite(ConvToScr(desc.pos), desc.n, 0); 2353 | RestoreFromROM(u16_to_ptr(SAUCER_ADDR), SAUCER_SIZE); 2354 | SoundBits3Off(0xfe); 2355 | } 2356 | 2357 | // Grab a copy of the saucer sprite descriptor, and set it up 2358 | // for rendering before returning it. 2359 | static SprDesc GetSaucerInfo() 2360 | { 2361 | // xref 0742 2362 | SprDesc desc = ReadDesc(&m.saucerDesc); 2363 | desc.sc = ConvToScr(desc.pos); 2364 | return desc; 2365 | } 2366 | 2367 | // Draw the player sprite 2368 | static void DrawPlayer() 2369 | { 2370 | // xref 036f 2371 | SprDesc desc = ReadDesc(&m.playerDesc); 2372 | 2373 | desc.sc = ConvToScr(desc.pos); 2374 | DrawSimpSprite(&desc); 2375 | 2376 | m.playerHeader.TimerExtra = 0; 2377 | } 2378 | 2379 | // Draw the saucer sprite 2380 | static void DrawSaucer() 2381 | { 2382 | // xref 073c 2383 | // xref 0742 2384 | SprDesc desc = GetSaucerInfo(); 2385 | DrawSimpSprite(&desc); 2386 | } 2387 | 2388 | // Wait for the player to press 1P or 2P, and then start the game 2389 | // with the appropriate flags. 2390 | // This loop is entered after the player has inserted a coin outside of game mode, see vblank_int() 2391 | static void WaitForStart() 2392 | { 2393 | // xref 076e 2394 | { 2395 | // xref 1979 2396 | // SuspendGameTasks 2397 | DsableGameTasks(); 2398 | DrawNumCredits(); 2399 | PrintCreditLabel(); 2400 | } 2401 | 2402 | ClearPlayField(); 2403 | PrintMessage(xytosc(96, 152), m.MSG_PUSH, 4); 2404 | 2405 | while (TRUE) 2406 | { 2407 | timeslice(); 2408 | 2409 | if ((m.numCoins - 1) != 0) 2410 | { 2411 | // Enough credits for either 1P or 2P start 2412 | PrintMessage(xytosc(32, 128), m.MSG_1_OR_2PLAYERS_BUTTON, 20); 2413 | 2414 | // Handle 1P or 2P 2415 | uint8_t inp = read_port(1); 2416 | 2417 | if (inp & 0x2) NewGame(1, 0x98, 0); 2418 | if (inp & 0x4) NewGame(0, 0x99, 0); 2419 | 2420 | continue; 2421 | } 2422 | 2423 | // Only enough credits for 1P start 2424 | PrintMessage(xytosc(32, 128), m.MSG_ONLY_1PLAYER__BUTTON, 20); 2425 | 2426 | // Break if 1P start hit. 2427 | if (read_port(1) & 0x4) 2428 | break; 2429 | } 2430 | 2431 | NewGame(0, 0x99, 0); 2432 | } 2433 | 2434 | // Starts a new game, and runs the game loop. 2435 | // This routine is entered via either the WaitForStart() loop after inserting coins 2436 | // outside of game mode, or is entered after the player dies via player_death() 2437 | // to continue the game. 2438 | // is2p - set to true if 2P was pressed 2439 | // cost - credits to deduct in bcd (0x99=1, 0x98=2 credits) 2440 | // skip - used to skip certain parts of initialization, used for continue 2441 | static void NewGame(uint8_t is2p, uint8_t cost, int skip) 2442 | { 2443 | // xref 0798 2444 | // xref 079b 2445 | int flags = ~skip; 2446 | 2447 | if (flags & INIT) 2448 | { 2449 | m.twoPlayers = is2p; 2450 | 2451 | { 2452 | uint8_t unused_carry = 0; 2453 | m.numCoins = bcd_add(m.numCoins, cost, &unused_carry); 2454 | } 2455 | 2456 | DrawNumCredits(); 2457 | 2458 | m.P1Scor.u16 = 0; 2459 | m.P2Scor.u16 = 0; 2460 | 2461 | PrintP1Score(); 2462 | PrintP2Score(); 2463 | 2464 | DsableGameTasks(); 2465 | 2466 | m.gameMode = 1; 2467 | 2468 | m.playerStates.u16 = 0x0101; // Both players alive 2469 | m.playerExtras.u16 = 0x0101; // Both players bonus available 2470 | 2471 | DrawStatus(); 2472 | DrawShieldP1(); 2473 | DrawShieldP2(); 2474 | 2475 | uint8_t ships = GetShipsPerCred(); 2476 | 2477 | m.p1ShipsRem = ships; 2478 | m.p2ShipsRem = ships; 2479 | 2480 | InitAlienRacks(); 2481 | 2482 | m.p1RackCnt = 0; 2483 | m.p2RackCnt = 0; 2484 | 2485 | InitAliensP1(); 2486 | InitAliensP2(); 2487 | 2488 | m.p1RefAlienPos = xytopix(24, 120); 2489 | m.p2RefAlienPos = xytopix(24, 120); 2490 | 2491 | CopyRAMMirror(); 2492 | RemoveShip(); 2493 | } 2494 | 2495 | if (flags & PROMPT) 2496 | { 2497 | PromptPlayer(); 2498 | ClearPlayField(); 2499 | m.isrSplashTask = 0; 2500 | } 2501 | 2502 | // xref 0804 top of new game loop 2503 | while (TRUE) 2504 | { 2505 | if (flags & SHIELDS) 2506 | { 2507 | DrawBottomLine(); 2508 | 2509 | if (m.playerDataMSB & 0x1) 2510 | { 2511 | RestoreShieldsP1(); 2512 | } 2513 | else 2514 | { 2515 | RestoreShieldsP2(); 2516 | DrawBottomLine(); 2517 | } 2518 | 2519 | // xref 0814 2520 | InitRack(); 2521 | } 2522 | else 2523 | { 2524 | flags |= SHIELDS; // don't skip next time 2525 | } 2526 | 2527 | EnableGameTasks(); 2528 | SoundBits3On(0x20); 2529 | 2530 | // xref 081f game loop 2531 | 2532 | while (TRUE) 2533 | { 2534 | PlrFireOrDemo(); 2535 | PlyrShotAndBump(); 2536 | CountAliens(); 2537 | 2538 | AdjustScore(); 2539 | 2540 | if (m.numAliens == 0) 2541 | { 2542 | HandleEndOfTurn(); 2543 | break; 2544 | } 2545 | 2546 | AShotReloadRate(); 2547 | CheckAndHandleExtraShipAward(); 2548 | SpeedShots(); 2549 | ShotSound(); 2550 | 2551 | if (! IsPlayerAlive()) 2552 | SoundBits3On(0x04); // Turn on player hit sound 2553 | 2554 | uint8_t w = FleetDelayExShip(); 2555 | write_port(6, w); // Feed the watchdog 2556 | CtrlSaucerSound(); 2557 | } 2558 | } 2559 | } 2560 | 2561 | // Get reference alien velocity, position and pointer for the current player 2562 | static uint8_t* GetRefAlienInfo(uint8_t *dxr, Word *pos) 2563 | { 2564 | // xref 0878 2565 | *dxr = m.refAlienDelta.x; 2566 | *pos = m.refAlienPos; 2567 | return GetAlRefPtr(); 2568 | } 2569 | 2570 | // Get reference alien pointer for the current player 2571 | static uint8_t* GetAlRefPtr() 2572 | { 2573 | // xref 0886 2574 | return (m.playerDataMSB & 0x1) ? &m.p1RefAlienPos.l : 2575 | &m.p2RefAlienPos.l; 2576 | } 2577 | 2578 | // Print "PLAY PLAYER" and flash the score at 15 hz for 3 seconds 2579 | // This is done upon starting a NewGame in 1P mode, or at the start 2580 | // of every turn in 2P mode. 2581 | static void PromptPlayer() 2582 | { 2583 | // xref 088d 2584 | 2585 | // "PLAY PLAYER<1>" 2586 | PrintMessage(xytosc(56,136), m.MSG_PLAY_PLAYER_1_, 14); 2587 | 2588 | // replace <1> with <2> 2589 | if ((m.playerDataMSB & 0x1) == 0) 2590 | DrawChar(xytosc(152, 136), 0x1c); 2591 | 2592 | m.isrDelay = 176; // 3 sec delay 2593 | 2594 | // xref 08a9 2595 | while (TRUE) 2596 | { 2597 | timeslice(); 2598 | uint8_t isrtick = m.isrDelay; 2599 | 2600 | if (isrtick == 0) 2601 | return; 2602 | 2603 | // Flash player score every 4 isrs 2604 | if (isrtick & 0x4) 2605 | { 2606 | // xref 08bc 2607 | Word sc = (m.playerDataMSB & 0x1) ? xytosc(24,224) : xytosc(168,224); 2608 | ClearSmallSprite(sc, 32, 0); 2609 | continue; 2610 | } 2611 | 2612 | DrawScore(GetScoreDescriptor()); 2613 | } 2614 | } 2615 | 2616 | // DIP5 and DIP3 control the number of extra lives the player starts with. 2617 | // DIP5 and DIP3 are wired into bits 1 and 0 of port 2 respectively. 2618 | // 2619 | // When read together as a two digit binary number, this is meant to be 2620 | // interpreted as the number of extra lives above the default of 3 that 2621 | // the player gets. 2622 | // 2623 | // 0 0 - 3 lives 2624 | // 0 1 - 4 lives 2625 | // 1 0 - 5 lives 2626 | // 1 1 - 6 lives 2627 | static uint8_t GetShipsPerCred() 2628 | { 2629 | // xref 08d1 2630 | return (read_port(2) & (DIP5_SHIPS2 | DIP3_SHIPS1)) + 3; 2631 | } 2632 | 2633 | // Increase alien shot speed when there are less than nine aliens on screen 2634 | static void SpeedShots() 2635 | { 2636 | // xref 08d8 2637 | if (m.numAliens >= 9) 2638 | return; 2639 | 2640 | m.alienShotDelta = -5; // from -4 to -5 2641 | } 2642 | 2643 | // Prints a text message (msg, n) on the screen at pos 2644 | // Used to print all the splash screen text, and other game messages 2645 | static void PrintMessage(Word sc, uint8_t* msg, size_t n) 2646 | { 2647 | // xref 08f3 2648 | // DebugMessage(sc, msg, n); 2649 | 2650 | for (size_t i = 0; i < n; ++i) 2651 | { 2652 | uint8_t c = msg[i]; 2653 | sc = DrawChar(sc, c); 2654 | } 2655 | } 2656 | 2657 | // Draw a text character c on the screen at pos 2658 | // Used by PrintMessage() 2659 | static Word DrawChar(Word sc, uint8_t c) 2660 | { 2661 | // xref 08ff 2662 | SprDesc desc; 2663 | desc.sc = sc; 2664 | desc.spr = u16_to_word(0x1e00 + c*8); 2665 | desc.n = 8; 2666 | 2667 | return DrawSimpSprite(&desc); 2668 | } 2669 | 2670 | // Timing logic that controls when the saucer appears (Every 25.6 secs) 2671 | // Called via vblank_int() 2672 | static void TimeToSaucer() 2673 | { 2674 | // xref 0913 2675 | // No ticking until alien rack has dropped down a bit 2676 | if (m.refAlienPos.y >= 120) 2677 | return; 2678 | 2679 | uint16_t timer = m.saucerTimer.u16; 2680 | 2681 | if (timer == 0) 2682 | { 2683 | timer = 0x600; // reset timer to 1536 game loops (25.6s) 2684 | m.saucerStart = 1; 2685 | } 2686 | 2687 | m.saucerTimer.u16 = timer - 1; 2688 | } 2689 | 2690 | // Get number of lives for the current player 2691 | static uint8_t GetNumberOfShips(uint8_t* *ptr) 2692 | { 2693 | // xref 092e 2694 | *ptr = (GetPlayerDataPtr() + 0xff); 2695 | return *(*ptr); 2696 | } 2697 | 2698 | // Award the one and only bonus life if the player's score is high enough, 2699 | // and fix up the lives indicators to reflect that. 2700 | static void CheckAndHandleExtraShipAward() 2701 | { 2702 | // xref 0935 2703 | if (*(CurPlyAlive() - 2) == 0) 2704 | return; 2705 | 2706 | // Bonus dip bit - award at 1000 or 1500 2707 | uint8_t b = (read_port(2) & DIP6_BONUS) ? 0x10 : 0x15; 2708 | uint8_t score_msb = *(GetScoreDescriptor() + 1); 2709 | 2710 | // score not high enough for bonus yet 2711 | if (score_msb < b) 2712 | return; 2713 | 2714 | uint8_t* nships_ptr; 2715 | GetNumberOfShips(&nships_ptr); 2716 | 2717 | // Award the bonus life 2718 | (*nships_ptr)++; 2719 | 2720 | int nships = *nships_ptr; 2721 | 2722 | SprDesc desc; 2723 | desc.sc = xytosc(8 + 16 * nships, 8); 2724 | desc.spr = ptr_to_word(m.PLAYER_SPRITES); 2725 | desc.n = 16; 2726 | 2727 | DrawSimpSprite(&desc); 2728 | 2729 | PrintNumShips(nships+1); 2730 | *(CurPlyAlive() - 2) = 0; // Flag extra ship has been awarded 2731 | m.extraHold = 0xff; // Handle Extra-ship sound 2732 | SoundBits3On(0x10); 2733 | } 2734 | 2735 | // Lookup score for alien based on the given row 2736 | static uint8_t* AlienScoreValue(uint8_t row) 2737 | { 2738 | // xref 097c 2739 | uint8_t si = 0; 2740 | 2741 | if (row < 2) si = 0; 2742 | else if (row < 4) si = 1; 2743 | else si = 2; 2744 | 2745 | return (m.AlienScores + si); 2746 | } 2747 | 2748 | // Add the score delta to the current player's score, and draw it. 2749 | // Called as part of the game loop. 2750 | // 2751 | // scoreDelta is modified in two places: 2752 | // PlayerShotHit() upon killing an alien (main thread) 2753 | // GameObj4() upon hitting the saucer (either vblank or mid depending on saucer x pos) 2754 | static void AdjustScore() 2755 | { 2756 | // xref 0988 2757 | uint8_t* sptr = GetScoreDescriptor(); 2758 | if (m.adjustScore == 0) return; 2759 | 2760 | m.adjustScore = 0; 2761 | 2762 | Word adj = m.scoreDelta; 2763 | uint8_t carry = 0; 2764 | 2765 | Word score; 2766 | 2767 | score.l = *(sptr); 2768 | score.l = bcd_add(score.l, adj.l, &carry); 2769 | *sptr = score.l; 2770 | 2771 | score.h = *(sptr+1); 2772 | score.h = bcd_add(score.h, adj.h, &carry); 2773 | *(sptr+1) = score.h; 2774 | 2775 | Word sc; 2776 | sc.l = *(sptr+2); 2777 | sc.h = *(sptr+3); 2778 | 2779 | Print4Digits(sc, score); 2780 | } 2781 | 2782 | // Print 4 digits using the bcd values in val.h and val.l 2783 | // Called via DrawScore and AdjustScore 2784 | static void Print4Digits(Word sc, Word val) 2785 | { 2786 | // xref 09ad 2787 | sc = DrawHexByte(sc, val.h); 2788 | sc = DrawHexByte(sc, val.l); 2789 | } 2790 | 2791 | // Draw the the hi and lo nibble of the bcd value in c at sc 2792 | static Word DrawHexByte(Word sc, uint8_t c) 2793 | { 2794 | // xref 09b2 2795 | sc = DrawHexByteSub(sc, c >> 4); 2796 | sc = DrawHexByteSub(sc, c & 0xf); 2797 | return sc; 2798 | } 2799 | 2800 | // Draw the digit in c at sc 2801 | static Word DrawHexByteSub(Word sc, uint8_t c) 2802 | { 2803 | // xref 09c5 2804 | return DrawChar(sc, c + 0x1a); 2805 | } 2806 | 2807 | // Return a pointer to the score info for the current player 2808 | static uint8_t* GetScoreDescriptor() 2809 | { 2810 | // xref 09ca 2811 | return (m.playerDataMSB & 0x1) ? &m.P1Scor.l : &m.P2Scor.l; 2812 | } 2813 | 2814 | // Clear the play field in the center of the screen. 2815 | // Horizontally, the play field is the full width of the screen. 2816 | // Vertically, the play field is the area above the lives and credits (16 pixels) 2817 | // and below the scores (32 pixels). 2818 | static void ClearPlayField() 2819 | { 2820 | // xref 09d6 2821 | uint8_t* screen = m.vram; 2822 | 2823 | for (int x = 0; x < 224; ++x) 2824 | { 2825 | screen += 2; 2826 | 2827 | for (int b = 0; b < 26; ++b) 2828 | *screen++ = 0; 2829 | 2830 | screen += 4; 2831 | } 2832 | } 2833 | 2834 | // Called from the game loop when the player has killed all aliens in the rack. 2835 | static void HandleEndOfTurn() 2836 | { 2837 | // xref 09ef 2838 | HandleEndOfTurnSub(); // wait for player to finish dying if necessary 2839 | 2840 | m.gameTasksRunning = 0; 2841 | ClearPlayField(); 2842 | 2843 | uint8_t pnum = m.playerDataMSB; 2844 | 2845 | CopyRAMMirror(); 2846 | 2847 | m.playerDataMSB = pnum; 2848 | pnum = m.playerDataMSB; // redundant load 2849 | 2850 | uint8_t rack_cnt = 0; 2851 | 2852 | uint8_t* rcptr = u16_to_ptr(pnum << 8 | 0xfe); 2853 | rack_cnt = (*rcptr % 8) + 1; 2854 | *rcptr = rack_cnt; 2855 | 2856 | // Starting Y coord for rack for new level 2857 | uint8_t y = m.AlienStartTable[rack_cnt-1]; 2858 | 2859 | uint8_t* refy = u16_to_ptr(pnum << 8 | 0xfc); 2860 | uint8_t* refx = refy + 1; 2861 | 2862 | *refy = y; 2863 | *refx = 56; 2864 | 2865 | if (!(pnum & 0x1)) 2866 | { 2867 | m.soundPort5 = 0x21; // start fleet with first sound 2868 | DrawShieldP2(); 2869 | InitAliensP2(); 2870 | } 2871 | else 2872 | { 2873 | DrawShieldP1(); 2874 | InitAliensP1(); 2875 | } 2876 | } 2877 | 2878 | // Called at start of HandleEndOfTurnSub() to handle the 2879 | // case of the player dying at the end of turn. 2880 | // (i.e. last alien and player both kill each other) 2881 | static void HandleEndOfTurnSub() 2882 | { 2883 | // xref 0a3c 2884 | 2885 | if (IsPlayerAlive()) 2886 | { 2887 | m.isrDelay = 48; // wait up to 3/4 of a sec 2888 | 2889 | do 2890 | { 2891 | // xref 0a47 2892 | timeslice(); // spin 2893 | if (m.isrDelay == 0) 2894 | return; 2895 | 2896 | } while (IsPlayerAlive()); 2897 | } 2898 | 2899 | // If player is not alive, wait for resurrection 2900 | while (!IsPlayerAlive()) 2901 | { 2902 | // xref 0a52 2903 | timeslice(); // spin 2904 | } 2905 | } 2906 | 2907 | // Returns 1 if player is alive, 0 otherwise 2908 | static uint8_t IsPlayerAlive() 2909 | { 2910 | // xref 0a59 2911 | return m.playerAlive == 0xff; 2912 | } 2913 | 2914 | // Called as part of the player bullet collision response in PlayerShotHit() 2915 | // when the player bullet kills an alien. 2916 | static void ScoreForAlien(uint8_t row) 2917 | { 2918 | // xref 0a5f 2919 | if (!m.gameMode) 2920 | return; 2921 | 2922 | SoundBits3On(0x08); 2923 | 2924 | uint8_t score = *(AlienScoreValue(row)); 2925 | 2926 | m.scoreDelta.h = 0; 2927 | m.scoreDelta.l = score; 2928 | m.adjustScore = 1; 2929 | } 2930 | 2931 | // Companion routine to SplashSprite 2932 | // Called from the main thread to initiate and wait for splash animations (CCOIN / PLAy). 2933 | static void Animate() 2934 | { 2935 | // xref 0a80 2936 | // Directs ISRSplTasks() (in vblank_int()) to call SplashSprite() 2937 | m.isrSplashTask = 2; 2938 | 2939 | // Spin until sprite in animation reaches its target position 2940 | do { 2941 | // xref 0a85 2942 | write_port(6, 2); // feed watchdog and spin 2943 | } 2944 | while (!m.splashReached); 2945 | 2946 | // Directs ISRSplTasks() to do nothing 2947 | m.isrSplashTask = 0; 2948 | } 2949 | 2950 | // Prints the animated text messages (such as "PLAY" "SPACE INVADERS"), 2951 | // by drawing the characters that make up the message with a short 2952 | // delay between them. (7 frames per character) 2953 | static Word PrintMessageDel(Word sc, uint8_t* str, uint8_t n) 2954 | { 2955 | // xref 0a93 2956 | // DebugMessage(sc, str, n); 2957 | 2958 | for (int i = 0; i < n; ++i) 2959 | { 2960 | sc = DrawChar(sc, str[i]); 2961 | m.isrDelay = 7; 2962 | while (m.isrDelay != 1) { timeslice(); } // spin 2963 | } 2964 | 2965 | return sc; 2966 | } 2967 | 2968 | // Need for the shooting C in CCOIN animation. 2969 | // Initiated in AnimateShootingSplashAlien(), and called 2970 | // via ISRSplTasks() during vblank_int() 2971 | static void SplashSquiggly() 2972 | { 2973 | // xref 0aab 2974 | // this works because this is the last game object. 2975 | RunGameObjs(u16_to_ptr(SQUIGGLY_SHOT_ADDR)); 2976 | } 2977 | 2978 | // Wait for approximately one second. (64 vblanks). 2979 | static void OneSecDelay() 2980 | { 2981 | // xref 0ab1 2982 | WaitOnDelay(64); 2983 | } 2984 | 2985 | // Wait for approximately two seconds. (128 vblanks). 2986 | static void TwoSecDelay() 2987 | { 2988 | // xref 0ab6 2989 | WaitOnDelay(128); 2990 | } 2991 | 2992 | // Runs the game objects in demo mode to attract players. 2993 | // Initiated from the main thread and called 2994 | // via ISRSplTasks() during vblank_int() 2995 | static void SplashDemo() 2996 | { 2997 | // xref 0abb 2998 | // xref 0072 2999 | m.shotSync = m.rolShotHeader.TimerExtra; 3000 | 3001 | DrawAlien(); 3002 | RunGameObjs(u16_to_ptr(PLAYER_ADDR)); // incl player 3003 | TimeToSaucer(); 3004 | } 3005 | 3006 | // Runs the appropriate splash screen task (from vblank_int()) 3007 | static void ISRSplTasks() 3008 | { 3009 | // xref 0abf 3010 | switch (m.isrSplashTask) 3011 | { 3012 | case 1: SplashDemo(); break; // Attract players with game demo 3013 | case 2: SplashSprite(); break; // Moves a sprite to a target location for an animation 3014 | case 4: SplashSquiggly(); break; // Run an alien shot for an animation ( CCOIN ) 3015 | } 3016 | } 3017 | 3018 | // Print an animated message in the center of the screen. 3019 | static void MessageToCenterOfScreen(uint8_t* str) 3020 | { 3021 | // xref 0acf 3022 | PrintMessageDel(xytosc(56,160), str, 0x0f); 3023 | } 3024 | 3025 | // Wait for n vblank interrupts to occur, using m.isrDelay 3026 | static void WaitOnDelay(uint8_t n) 3027 | { 3028 | // xref 0ad7 3029 | // Wait on ISR counter to reach 0 3030 | m.isrDelay = n; 3031 | 3032 | while (m.isrDelay != 0) { timeslice(); } // spin 3033 | } 3034 | 3035 | // Copy src into the splash animation structure. 3036 | // The four animations copied this way are 3037 | // 3038 | // (for PLAy animation) 3039 | // 3040 | // 0x1a95 - Move alien left to grab y 3041 | // 0x1bb0 - Move alien (with y) to right edge 3042 | // 0x1fc9 - Bring alien back (with Y) to message 3043 | // 3044 | // (for CCOIN animation) 3045 | // 3046 | // 0x1fd5 - Move alien to point above extra 'C' 3047 | static void IniSplashAni(uint8_t* src) 3048 | { 3049 | // xref 0ae2 3050 | BlockCopy(&m.splashAnForm, src, 12); 3051 | } 3052 | 3053 | // Called during (splash screens) to do some miscellaneous tasks 3054 | // a) Player shot collision response 3055 | // b) Detecting and handling the alien rack bumping the screen edges 3056 | // c) Checking for TAITO COP input sequence 3057 | static uint8_t CheckPlyrShotAndBump() 3058 | { 3059 | // xref 0bf1 3060 | PlyrShotAndBump(); 3061 | CheckHiddenMes(); 3062 | return 0xff; 3063 | } 3064 | 3065 | // Erases a sprite by clearing the four bytes it 3066 | // could possibly be in. 3067 | static void EraseSimpleSprite(Word pos, uint8_t n) 3068 | { 3069 | // xref 1424 3070 | Word sc = CnvtPixNumber(pos); 3071 | 3072 | for (int i = 0; i < n; ++i, sc.u16 += xysc(1, 0)) 3073 | { 3074 | uint8_t* screen = word_to_ptr(sc); 3075 | 3076 | *screen++ = 0; 3077 | *screen++ = 0; 3078 | } 3079 | } 3080 | 3081 | // Draws a non shifted sprite from desc->spr horizontally 3082 | // across the screen at desc->pos for desc->n bytes. 3083 | // Each byte of the sprite is a vertical 8 pixel strip 3084 | static Word DrawSimpSprite(SprDesc *desc) 3085 | { 3086 | // xref 1439 3087 | uint8_t* screen = word_to_ptr(desc->sc); 3088 | uint8_t* sprite = word_to_ptr(desc->spr); 3089 | 3090 | for (size_t i = 0; i < desc->n; ++i, screen += xysc(1, 0)) 3091 | *screen = sprite[i]; 3092 | 3093 | return ptr_to_word(screen); 3094 | } 3095 | 3096 | // Using pixnum, set the shift count on the hardware shifter 3097 | // and return the screen coordinates for rendering 3098 | static Word CnvtPixNumber(Word pos) 3099 | { 3100 | // xref 1474 3101 | write_port(2, (pos.u16 & 0xff) & 0x07); 3102 | return ConvToScr(pos); 3103 | } 3104 | 3105 | // Draw a shifted sprite to the screen, blending with screen contents 3106 | static void DrawShiftedSprite(struct SprDesc *desc) 3107 | { 3108 | // xref 1400 3109 | DrawSpriteGeneric(desc, OP_BLEND); 3110 | } 3111 | 3112 | // Erase a shifted sprite from the screen, zeroing screen contents 3113 | static void EraseShifted(struct SprDesc *desc) 3114 | { 3115 | // xref 1452 3116 | DrawSpriteGeneric(desc, OP_ERASE); 3117 | } 3118 | 3119 | // Draw a shifted sprite to the screen, blending with screen contents, 3120 | // and detect if drawn sprite collided with existing pixels 3121 | static void DrawSprCollision(struct SprDesc *desc) 3122 | { 3123 | // xref 1491 3124 | DrawSpriteGeneric(desc, OP_COLLIDE); 3125 | } 3126 | 3127 | // Draw a shifted sprite to the screen, overwriting screen contents. 3128 | static void DrawSprite(struct SprDesc *desc) 3129 | { 3130 | // xref 15d3 3131 | DrawSpriteGeneric(desc, OP_BLIT); 3132 | } 3133 | 3134 | // Generic sprite drawing routine for shifted spries 3135 | // desc->spr - source pointer 3136 | // desc->pixnum - pixel position to draw at 3137 | // desc->n - width 3138 | // op - erase | blit | blend | collide 3139 | static void DrawSpriteGeneric(struct SprDesc *desc, int op) 3140 | { 3141 | Word sc = CnvtPixNumber(desc->pos); 3142 | uint8_t* sprite = word_to_ptr(desc->spr); 3143 | 3144 | if (op == OP_COLLIDE) 3145 | m.collision = 0; 3146 | 3147 | for (int i = 0; i < desc->n; ++i, sc.u16 += xysc(1,0)) 3148 | { 3149 | uint8_t* screen = word_to_ptr(sc); 3150 | 3151 | uint8_t shift_in[2]; 3152 | shift_in[0] = sprite[i]; 3153 | shift_in[1] = 0; 3154 | 3155 | for (int j = 0; j < 2; ++j) 3156 | { 3157 | write_port(4, shift_in[j]); // write into shift reg 3158 | uint8_t shifted = read_port(3); // get the shifted pixels (shift based on pix num) 3159 | 3160 | if (op == OP_COLLIDE && (shifted & *screen)) 3161 | m.collision = 1; 3162 | 3163 | if (op == OP_COLLIDE || op == OP_BLEND) 3164 | *screen = shifted | *screen; 3165 | else if (op == OP_BLIT) 3166 | *screen = shifted; 3167 | else if (op == OP_ERASE) 3168 | *screen = (shifted ^ 0xff) & *screen; 3169 | 3170 | screen++; 3171 | } 3172 | } 3173 | } 3174 | 3175 | // Repeat (width n) the pixel strip in byte v horizontally across the screen 3176 | static Word ClearSmallSprite(Word sc, uint8_t n, uint8_t v) 3177 | { 3178 | // xref 14cb 3179 | for (int i = 0; i < n; ++i, sc.u16 += xysc(1,0)) 3180 | *(word_to_ptr(sc)) = v; 3181 | 3182 | return sc; 3183 | } 3184 | 3185 | // Player bullet collision response 3186 | static void PlayerShotHit() 3187 | { 3188 | // xref 14d8 3189 | uint8_t status = m.plyrShotStatus; 3190 | 3191 | // if alien explosion state, bail 3192 | if (status == 5) 3193 | return; 3194 | 3195 | // if not normal movement, bail 3196 | if (status != 2) 3197 | return; 3198 | 3199 | // Get the Y coord 3200 | uint8_t shoty = m.playerShotDesc.pos.y; 3201 | 3202 | if (shoty >= 216) 3203 | { 3204 | // missed and hit top of screen 3205 | m.plyrShotStatus = 3; 3206 | m.alienIsExploding = 0; 3207 | SoundBits3Off(0xf7); 3208 | return; 3209 | } 3210 | 3211 | // xref 14ea 3212 | if (!m.alienIsExploding) 3213 | return; 3214 | 3215 | if (shoty >= 206) 3216 | { 3217 | // hit the saucer 3218 | // xref 1579 3219 | m.saucerHit = 1; 3220 | m.plyrShotStatus = 4; 3221 | 3222 | // xref 154a 3223 | m.alienIsExploding = 0; 3224 | SoundBits3Off(0xf7); 3225 | return; 3226 | } 3227 | 3228 | shoty += 6; 3229 | 3230 | { 3231 | uint8_t refy = m.refAlienPos.y; 3232 | 3233 | // refy can wrap around, if the topmost alien row gets near the bottom of the screen 3234 | // in usual play, refy will be < 144. 3235 | if ((refy < 144) && (refy >= shoty)) 3236 | { 3237 | // hit the shields 3238 | m.plyrShotStatus = 3; 3239 | m.alienIsExploding = 0; 3240 | SoundBits3Off(0xf7); 3241 | return; 3242 | } 3243 | } 3244 | 3245 | // xref 1504 3246 | // Get here if player shot hits an alien or an alien shot. 3247 | // There is a subtle bug here, see CodeBug1 in CA 3248 | Word res = FindRow(shoty); 3249 | 3250 | uint8_t row = res.h; 3251 | uint8_t ay = res.l; 3252 | 3253 | res = FindColumn(m.playerShotDesc.pos.x); 3254 | 3255 | uint8_t col = res.h; 3256 | uint8_t ax = res.l; 3257 | 3258 | m.expAlien.pos = u8_u8_to_word(ax, ay); 3259 | m.plyrShotStatus = 5; 3260 | 3261 | uint8_t* alienptr = GetAlienStatPtr(row, col); 3262 | 3263 | if (*alienptr == 0) 3264 | { 3265 | // If alien is dead, then the player shot must have hit an alien shot 3266 | m.plyrShotStatus = 3; 3267 | m.alienIsExploding = 0; 3268 | SoundBits3Off(0xf7); 3269 | return; 3270 | } 3271 | 3272 | // Kill the alien 3273 | *alienptr = 0; 3274 | ScoreForAlien(row); 3275 | SprDesc desc = ReadDesc(&m.expAlien); 3276 | DrawSprite(&desc); 3277 | m.expAlienTimer = 16; 3278 | } 3279 | 3280 | // Counts the number of 16s between *v and target. 3281 | // This is roughly (tgt - *v) / 16. 3282 | static uint8_t Cnt16s(uint8_t *v, uint8_t tgt) 3283 | { 3284 | // xref 1554 3285 | uint8_t n = 0; 3286 | 3287 | if ((*v) >= tgt) 3288 | { 3289 | do 3290 | { 3291 | // wrap ref 3292 | n++; 3293 | (*v) += 16; 3294 | 3295 | } while ((*v) & 0x80); 3296 | } 3297 | 3298 | while ((*v) < tgt) 3299 | { 3300 | (*v) += 16; 3301 | n++; 3302 | } 3303 | 3304 | return n; 3305 | } 3306 | 3307 | // Find alien row given y pos 3308 | static Word FindRow(uint8_t y) 3309 | { 3310 | // xref 1562 3311 | uint8_t ry = m.refAlienPos.y; 3312 | uint8_t rnum = Cnt16s(&ry, y) - 1; 3313 | uint8_t coord = (ry - 16); 3314 | 3315 | return u8_u8_to_word(rnum, coord); 3316 | } 3317 | 3318 | // Find alien column given x pos 3319 | static Word FindColumn(uint8_t x) 3320 | { 3321 | // xref 156f 3322 | uint8_t rx = m.refAlienPos.x; 3323 | uint8_t cnum = Cnt16s(&rx, x); 3324 | uint8_t coord = (rx - 16); 3325 | 3326 | return u8_u8_to_word(cnum, coord); 3327 | } 3328 | 3329 | // Return a pointer to the alien status for the current player 3330 | // given the row and column of the alien 3331 | static uint8_t* GetAlienStatPtr(uint8_t row, uint8_t col) 3332 | { 3333 | // xref 1581 3334 | // row is 0 based 3335 | // col is 1 based 3336 | 3337 | uint8_t idx = row * 11 + (col - 1); 3338 | return u16_to_ptr(m.playerDataMSB << 8 | idx); 3339 | } 3340 | 3341 | // Change alien deltaX and deltaY when alien rack bumps edges 3342 | static void RackBump() 3343 | { 3344 | // xref 1597 3345 | // Change alien deltaX and deltaY when rack bumps edges 3346 | uint8_t dx = 0; 3347 | uint8_t dir = 0; 3348 | 3349 | if (m.rackDirection == 0) 3350 | { 3351 | // xref 159e check right edge 3352 | if (!RackBumpEdge(xytosc(213,32))) 3353 | return; 3354 | 3355 | dx = -2; 3356 | dir = 1; // rack now moving left 3357 | } 3358 | else 3359 | { 3360 | // check left edge 3361 | if (!RackBumpEdge(xytosc(9,32))) 3362 | return; 3363 | 3364 | // rack now moving right 3365 | // inline 18f1 3366 | dx = m.numAliens == 1 ? 3 : 2; // go faster if only one alien remaining 3367 | dir = 0; // rack now moving right 3368 | } 3369 | 3370 | m.rackDirection = dir; 3371 | m.refAlienDelta.x = dx; 3372 | m.refAlienDelta.y = m.rackDownDelta; 3373 | } 3374 | 3375 | // Check 23 bytes vertically up from sc for pixels. 3376 | // Used by RackBump to detect whether alien rack is hitting the edges of the play area. 3377 | static uint8_t RackBumpEdge(Word sc) 3378 | { 3379 | // xref 15c5 3380 | uint8_t* screen = word_to_ptr(sc); 3381 | 3382 | for (int i = 0; i < 23; ++i) 3383 | { 3384 | timeslice(); 3385 | 3386 | // found some pixels 3387 | if (*screen++) 3388 | return 1; 3389 | } 3390 | 3391 | return 0; 3392 | } 3393 | 3394 | // Count the number of live aliens for the current player 3395 | static void CountAliens() 3396 | { 3397 | // xref 15f3 3398 | uint8_t* iter = GetPlayerDataPtr(); // Get active player descriptor 3399 | uint8_t n = 0; 3400 | 3401 | for (int i = 0; i < 55; ++i) 3402 | { 3403 | timeslice(); 3404 | 3405 | if (*iter++ != 0) 3406 | ++n; 3407 | } 3408 | 3409 | m.numAliens = n; 3410 | 3411 | if (n != 1) 3412 | return; 3413 | 3414 | // Apparently unused 3415 | *(u16_to_ptr(0x206b)) = 1; 3416 | } 3417 | 3418 | // Return a pointer the the current player's RAM area 3419 | static uint8_t* GetPlayerDataPtr() 3420 | { 3421 | // xref 1611 3422 | return u16_to_ptr(m.playerDataMSB << 8); 3423 | } 3424 | 3425 | // Handles player firing in game and demo mode. 3426 | // In both cases, nothing happens if there is a shot on the screen. 3427 | // In game mode, reads the fire button and debounces to detect press. 3428 | // In demo mode, initiates a shot always, and consumes the next movement command from the buffer. 3429 | static void PlrFireOrDemo() 3430 | { 3431 | // xref 1618 3432 | if (m.playerAlive != 0xff) 3433 | return; 3434 | 3435 | uint8_t timer_hi = m.playerHeader.TimerMSB; 3436 | uint8_t timer_lo = m.playerHeader.TimerLSB; 3437 | 3438 | // Return if not ready yet. 3439 | if (timer_lo | timer_hi) 3440 | return; 3441 | 3442 | // Return if player has a shot still on screen 3443 | if (m.plyrShotStatus) 3444 | return; 3445 | 3446 | // xref 162b 3447 | if (m.gameMode) 3448 | { 3449 | // Handle fire button reading and debouncing 3450 | uint8_t prev = m.fireBounce; 3451 | uint8_t cur = (ReadInputs() & 0x10) != 0; 3452 | 3453 | if (prev == cur) 3454 | return; 3455 | 3456 | if (cur) 3457 | m.plyrShotStatus = 1; // Flag shot active 3458 | 3459 | m.fireBounce = cur; 3460 | return; 3461 | } 3462 | else 3463 | { 3464 | // Demo player constantly fires 3465 | 3466 | m.plyrShotStatus = 1; 3467 | 3468 | // Consume demo command from circular buffer 0x1f74 <-> 0x1f7e 3469 | // 3470 | // DemoCommands: 3471 | //; (1=Right, 2=Left) 3472 | // 74 75 76 77 78 79 7A 7B 7C 7D 7E 3473 | //1F74: 01 01 00 00 01 00 02 01 00 02 01 3474 | 3475 | Word iter = u16_to_word(m.demoCmdPtr.u16 + 1); 3476 | 3477 | if (iter.l >= 0x7e) 3478 | iter.l = 0x74; // wrap 3479 | 3480 | m.demoCmdPtr = iter; 3481 | m.nextDemoCmd = *(word_to_ptr(iter)); 3482 | } 3483 | } 3484 | 3485 | // Called when all players are dead in game mode 3486 | // Prints "GAME OVER" and sets things up to reenter splash screens. 3487 | static void GameOver() 3488 | { 3489 | // xref 16c9 3490 | 3491 | PrintMessageDel(xytosc(72,192), m.MSG_GAME_OVER__PLAYER___, 10); 3492 | TwoSecDelay(); 3493 | ClearPlayField(); 3494 | m.gameMode = 0; 3495 | write_port(5, 0); // all sound off 3496 | EnableGameTasks(); 3497 | main_init(1); 3498 | assert(FALSE); // won't return 3499 | } 3500 | 3501 | // Called when the player loses the game upon an alien reaching the bottom 3502 | static void on_invaded() 3503 | { 3504 | // xref 16ea 3505 | m.playerAlive = 0; 3506 | 3507 | do { 3508 | PlayerShotHit(); 3509 | SoundBits3On(4); 3510 | } 3511 | while (!IsPlayerAlive()); 3512 | 3513 | DsableGameTasks(); 3514 | DrawNumShipsSub(xytosc(24, 8)); // 19fa 3515 | PrintNumShips(0); 3516 | 3517 | // xref 196b 3518 | SoundBits3Off(0xfb); 3519 | player_death(1); 3520 | 3521 | // won't return 3522 | assert(FALSE); 3523 | } 3524 | 3525 | // Increases the difficulty of the game as the player's score gets higher by 3526 | // decreasing time between alien shots. 3527 | static void AShotReloadRate() 3528 | { 3529 | // xref 170e 3530 | uint8_t score_hi = *(GetScoreDescriptor() + 1); 3531 | 3532 | // Uses these tables, in decimal 3533 | // 02 16 32 48 (AReloadScoreTab) 3534 | // 48 16 11 08 07 (ShotReloadRate) 3535 | 3536 | int i = 0; 3537 | 3538 | // xref 171c 3539 | for (i = 0; i < 4; ++i) 3540 | { 3541 | if (m.AReloadScoreTab[i] >= score_hi) 3542 | break; 3543 | } 3544 | 3545 | // xref 1727 3546 | m.aShotReloadRate = m.ShotReloadRate[i]; 3547 | } 3548 | 3549 | // Turn player shot sound on/off depending on m.plyrShotStatus 3550 | static void ShotSound() 3551 | { 3552 | // xref 172c 3553 | if (m.plyrShotStatus == 0) 3554 | { 3555 | SoundBits3Off(0xfd); 3556 | return; 3557 | } 3558 | 3559 | SoundBits3On(0x02); 3560 | } 3561 | 3562 | // Ticks down and reset the timers that determines when the alien sound is changed. 3563 | // The sound is actually changed in FleetDelayExShip() 3564 | static void TimeFleetSound() 3565 | { 3566 | // xref 1740 3567 | if (--m.fleetSndHold == 0) 3568 | DisableFleetSound(); 3569 | 3570 | if (m.playerOK == 0) 3571 | { 3572 | DisableFleetSound(); 3573 | return; 3574 | } 3575 | 3576 | if (--m.fleetSndCnt != 0) 3577 | return; 3578 | 3579 | write_port(5, m.soundPort5); 3580 | 3581 | if (m.numAliens == 0) 3582 | { 3583 | DisableFleetSound(); 3584 | return; 3585 | } 3586 | 3587 | m.fleetSndCnt = m.fleetSndReload; 3588 | m.changeFleetSnd = 0x01; 3589 | m.fleetSndHold = 0x04; 3590 | } 3591 | 3592 | // Turn off the fleet movement sounds 3593 | static void DisableFleetSound() 3594 | { 3595 | // xref 176d 3596 | SetSoundWithoutFleet(m.soundPort5); 3597 | } 3598 | 3599 | // Mask fleet movement sound off from given byte, and use it to set sound 3600 | static void SetSoundWithoutFleet(uint8_t v) 3601 | { 3602 | // xref 1770 3603 | write_port(5, v & 0x30); 3604 | } 3605 | 3606 | // Handles rotating the fleet sounds if it is time to do so, and determines the 3607 | // delay between them using a table keyed by the number of live aliens. 3608 | // The bonus ship sound is also handled here. 3609 | static uint8_t FleetDelayExShip() 3610 | { 3611 | // xref 1775 3612 | // The two sound tables (in decimal): 3613 | // [ 50 43 36 28 22 17 13 10 08 07 06 05 04 03 02 01 ] (soundDelayKey) 3614 | // [ 52 46 39 34 28 24 21 19 16 14 13 12 11 09 07 05 ] (soundDelayValue) 3615 | 3616 | uint8_t snd_b = 0; 3617 | uint8_t snd_a = 0; 3618 | 3619 | if (m.changeFleetSnd) 3620 | { 3621 | uint8_t n = m.numAliens; 3622 | 3623 | // xref 1785 3624 | int i = 0; 3625 | 3626 | for (i = 0; i < 16; ++i) 3627 | { 3628 | if (n >= m.soundDelayKey[i]) 3629 | break; 3630 | } 3631 | 3632 | m.fleetSndReload = m.soundDelayValue[i]; 3633 | 3634 | snd_b = m.soundPort5 & 0x30; // Mask off all fleet movement sounds 3635 | snd_a = m.soundPort5 & 0x0f; // Mask off all except fleet sounds 3636 | 3637 | // Rotate to next sound and wrap if neccessary 3638 | snd_a = (snd_a << 1) | (snd_a >> 7); 3639 | 3640 | if (snd_a == 0x10) 3641 | snd_a = 0x01; 3642 | 3643 | m.soundPort5 = snd_a | snd_b; 3644 | m.changeFleetSnd = 0; 3645 | } 3646 | 3647 | // xref 17aa 3648 | if (--m.extraHold == 0) 3649 | SoundBits3Off(0xef); 3650 | 3651 | return snd_a; 3652 | } 3653 | 3654 | // Read the input port corresponding to the current player. 3655 | static uint8_t ReadInputs() 3656 | { 3657 | // xref 17c0 3658 | uint8_t port = m.playerDataMSB & 0x1 ? 1 : 2; 3659 | return read_port(port); 3660 | } 3661 | 3662 | // End the game if tilt switch is activated. 3663 | static void CheckHandleTilt() 3664 | { 3665 | // xref 17cd 3666 | if (!(read_port(2) & TILT_BIT)) return; 3667 | if (m.tilt) return; 3668 | 3669 | yield(YIELD_TILT); 3670 | assert(FALSE); 3671 | } 3672 | 3673 | // This routine is entered after the stack reset in CheckHandleTilt 3674 | // It prints the tilt message and ends the game, cycling back to the splash screen. 3675 | static void on_tilt() 3676 | { 3677 | // xref 17dc 3678 | for (size_t i = 0; i < 4; ++i) 3679 | ClearPlayField(); 3680 | 3681 | m.tilt = 1; 3682 | DsableGameTasks(); 3683 | enable_interrupts(); 3684 | 3685 | PrintMessageDel(xytosc(96,176), m.MSG_TILT, 4); 3686 | OneSecDelay(); 3687 | 3688 | m.tilt = 0; 3689 | m.waitStartLoop = 0; 3690 | 3691 | GameOver(); // does not return. 3692 | assert(FALSE); 3693 | } 3694 | 3695 | // Play appropriate sounds based on saucer state. 3696 | static void CtrlSaucerSound() 3697 | { 3698 | // xref 1804 3699 | if (m.saucerActive == 0) 3700 | { 3701 | SoundBits3Off(0xfe); 3702 | return; 3703 | } 3704 | 3705 | if (m.saucerHit) 3706 | return; 3707 | 3708 | SoundBits3On(0x01); 3709 | } 3710 | 3711 | // Draws the text and sprites for the "SCORE ADVANCE TABLE" in the splash screens. 3712 | static void DrawAdvTable() 3713 | { 3714 | // xref 1815 3715 | PrintMessage(xytosc(32,128), m.MSG__SCORE_ADVANCE_TABLE_, 0x15); 3716 | 3717 | // PrintMessageAdv uses this 3718 | m.temp206C = 10; 3719 | 3720 | PriCursor cursor; 3721 | 3722 | // Sprite display list for score advance table 3723 | cursor.src = m.SCORE_ADV_SPRITE_LIST; 3724 | 3725 | while (!ReadPriStruct(&cursor)) 3726 | Draw16ByteSprite(cursor.sc, cursor.obj); 3727 | 3728 | // Message display list for score advance table 3729 | cursor.src = m.SCORE_ADV_MSG_LIST; 3730 | 3731 | while (!ReadPriStruct(&cursor)) 3732 | PrintMessageAdv(cursor.sc, cursor.obj); 3733 | } 3734 | 3735 | // Used when drawing the Score Advance Table to draw the alien and saucer sprites 3736 | static void Draw16ByteSprite(Word sc, uint8_t* sprite) 3737 | { 3738 | // xref 1844 3739 | SprDesc desc; 3740 | desc.sc = sc; 3741 | desc.spr = ptr_to_word(sprite); 3742 | desc.n = 16; 3743 | 3744 | DrawSimpSprite(&desc); 3745 | } 3746 | 3747 | // Used when drawing the Score Advance Table to draw the text 3748 | static void PrintMessageAdv(Word sc, uint8_t* msg) 3749 | { 3750 | // xref 184c 3751 | size_t n = m.temp206C; 3752 | PrintMessageDel(sc, msg, n); 3753 | } 3754 | 3755 | // A PriStruct is a display list, containing a list of 3756 | // objects to display (either sprites or text), and their position. 3757 | // This routine is used to iterate through the list and read each member. 3758 | static int ReadPriStruct(PriCursor *pri) 3759 | { 3760 | // xref 1856 3761 | pri->sc.l = *pri->src++; 3762 | 3763 | if (pri->sc.l == 0xff) 3764 | return TRUE; // hit end 3765 | 3766 | pri->sc.h = *pri->src++; 3767 | 3768 | Word obj; 3769 | obj.l = *pri->src++; 3770 | obj.h = *pri->src++; 3771 | 3772 | pri->obj = word_to_ptr(obj); 3773 | 3774 | return FALSE; // keep going 3775 | } 3776 | 3777 | // Required for CCOIN and PLAy splash animations 3778 | // The animation structure used here is set up by IniSplashAni() 3779 | // Moves and animates a sprite until it reaches a target position 3780 | static void SplashSprite() 3781 | { 3782 | // xref 1868 3783 | ++m.splashAnForm; 3784 | uint8_t* vptr = &m.splashDelta.y; 3785 | 3786 | uint8_t dy = *vptr; 3787 | uint8_t x = AddDelta(vptr, dy); 3788 | 3789 | if (m.splashTargetX != x) 3790 | { 3791 | uint8_t flip = m.splashAnForm & 0x04; 3792 | Word spr = m.splashImRest; 3793 | 3794 | if (flip == 0) { spr.u16 += 48; } 3795 | 3796 | m.splashPic = spr; 3797 | 3798 | SprDesc desc = ReadDesc((SprDesc*) u16_to_ptr(SPLASH_DESC_ADDR)); 3799 | 3800 | // splash desc is out of order, needs swapping. 3801 | Word tmp = desc.pos; 3802 | 3803 | desc.pos = desc.spr; 3804 | desc.spr = tmp; 3805 | 3806 | DrawSprite(&desc); 3807 | return; 3808 | } 3809 | 3810 | // xref 1898 3811 | m.splashReached = 1; 3812 | } 3813 | 3814 | // Handles the shooting part of the CCOIN splash animation 3815 | // Companion routine is SplashSquiggly() 3816 | static void AnimateShootingSplashAlien() 3817 | { 3818 | // xref 189e 3819 | BlockCopy(u16_to_ptr(SQUIGGLY_SHOT_ADDR), m.SPLASH_SHOT_OBJDATA, 16); 3820 | 3821 | m.shotSync = 2; 3822 | m.alienShotDelta = 0xff; 3823 | m.isrSplashTask = 4; 3824 | 3825 | // spin until shot collides 3826 | while ((m.squShotData.Status & SHOT_BLOWUP) == 0) 3827 | { 3828 | // xref 18b8 3829 | timeslice(); // spin 3830 | } 3831 | 3832 | // spin until shot explosion finishes 3833 | while ((m.squShotData.Status & SHOT_BLOWUP) != 0) 3834 | { 3835 | // xref 18c0 3836 | timeslice(); // spin 3837 | } 3838 | 3839 | // replace extra 'C' with blank space 3840 | DrawChar(xytosc(120,136), 0x26); 3841 | 3842 | TwoSecDelay(); 3843 | } 3844 | 3845 | // Handle initialization and splash screens. 3846 | // Initially entered upon startup via reset(), with skip=0 3847 | // Is is terminated when the stack is reset upon the insertion of credits, and is 3848 | // replaced by WaitForStart() 3849 | // The routine is eventually re-entered with skip=0 via GameOver() 3850 | static void main_init(int skip) 3851 | { 3852 | // xref 18d4 3853 | int init = !skip; 3854 | 3855 | if (init) 3856 | { 3857 | CopyRomtoRam(); 3858 | DrawStatus(); 3859 | } 3860 | 3861 | // 18df 3862 | while (TRUE) 3863 | { 3864 | if (init) 3865 | { 3866 | m.aShotReloadRate = 8; 3867 | 3868 | write_port(3, 0); // turn off sound 1 3869 | write_port(5, 0); // turn off sound 2 3870 | 3871 | // SetISRSplashTask 3872 | m.isrSplashTask = 0; 3873 | 3874 | // xref 0af2 3875 | enable_interrupts(); 3876 | 3877 | OneSecDelay(); 3878 | 3879 | // xref 0af6 3880 | PrintMessageDel(xytosc(96, 184), 3881 | m.splashAnimate ? m.MSG_PLAY : m.MSG_PLAY2, 3882 | 4); 3883 | 3884 | // xref 0b08 3885 | MessageToCenterOfScreen(m.MSG_SPACE__INVADERS); 3886 | 3887 | // xref 0b0e 3888 | OneSecDelay(); 3889 | DrawAdvTable(); 3890 | TwoSecDelay(); 3891 | 3892 | // xref 0b17 3893 | if (m.splashAnimate == 0) 3894 | { 3895 | // run script for alien twiddling with upside down 'Y' 3896 | IniSplashAni(u16_to_ptr(0x1a95)); 3897 | Animate(); 3898 | IniSplashAni(u16_to_ptr(0x1bb0)); 3899 | Animate(); 3900 | 3901 | OneSecDelay(); 3902 | 3903 | IniSplashAni(u16_to_ptr(0x1fc9)); 3904 | Animate(); 3905 | 3906 | OneSecDelay(); 3907 | 3908 | ClearSmallSprite(xytosc(125, 184), 10, 0); 3909 | TwoSecDelay(); 3910 | } 3911 | 3912 | // xref 0b4a 3913 | ClearPlayField(); 3914 | 3915 | if (m.p1ShipsRem == 0) 3916 | { 3917 | // xref 0b54 3918 | m.p1ShipsRem = GetShipsPerCred(); 3919 | RemoveShip(); 3920 | } 3921 | 3922 | // xref 0b5d 3923 | CopyRAMMirror(); 3924 | InitAliensP1(); 3925 | 3926 | DrawShieldP1(); 3927 | RestoreShieldsP1(); 3928 | m.isrSplashTask = 1; 3929 | 3930 | DrawBottomLine(); 3931 | 3932 | // xref 0b71 3933 | 3934 | do 3935 | { 3936 | PlrFireOrDemo(); 3937 | 3938 | // xref 0b74 3939 | // check player shot, and aliens bumping screen, also handle hidden message 3940 | uint8_t a2 = CheckPlyrShotAndBump(); 3941 | 3942 | // feed the watchdog 3943 | write_port(6, a2); 3944 | 3945 | } while (IsPlayerAlive()); 3946 | 3947 | // xref 0b7f 3948 | m.plyrShotStatus = 0; 3949 | 3950 | // wait for demo player to finish exploding. 3951 | while (! IsPlayerAlive() ) 3952 | { 3953 | // xref 0b83 3954 | timeslice(); // spin 3955 | } 3956 | } 3957 | 3958 | // xref 0b89 3959 | m.isrSplashTask = 0; 3960 | OneSecDelay(); 3961 | ClearPlayField(); 3962 | 3963 | PrintMessage(xytosc(64,136), m.MSG_INSERT__COIN, 12); 3964 | 3965 | // draw extra 'C' for CCOIN 3966 | if (m.splashAnimate == 0) 3967 | DrawChar(xytosc(120,136), 2); 3968 | 3969 | PriCursor cursor; 3970 | cursor.src = m.CREDIT_TABLE; 3971 | 3972 | ReadPriStruct(&cursor); 3973 | PrintMessageAdv(cursor.sc, cursor.obj); 3974 | 3975 | // Only print coin info if DIP7 is set 3976 | if ((read_port(2) & DIP7_COININFO) == 0) 3977 | { 3978 | cursor.src = m.CREDIT_TABLE_COINS; 3979 | 3980 | // xref 183a 3981 | while (!ReadPriStruct(&cursor)) 3982 | PrintMessageAdv(cursor.sc, cursor.obj); 3983 | } 3984 | 3985 | TwoSecDelay(); 3986 | 3987 | // xref 0bc6 3988 | if (m.splashAnimate == 0) 3989 | { 3990 | // xref 0bce 3991 | // shoot C animation 3992 | IniSplashAni(u16_to_ptr(0x1fd5)); 3993 | Animate(); 3994 | AnimateShootingSplashAlien(); 3995 | } 3996 | 3997 | // xref 0bda 3998 | m.splashAnimate = !m.splashAnimate; 3999 | ClearPlayField(); 4000 | } 4001 | } 4002 | 4003 | // Return pointer to non current player's alive status 4004 | static uint8_t* GetOtherPlayerAliveFlag() 4005 | { 4006 | // xref 18e7 4007 | return (m.playerDataMSB & 0x1 ? &m.playerStates.h : &m.playerStates.l); 4008 | } 4009 | 4010 | // Use a mask to enable sounds on port 3 4011 | static void SoundBits3On(uint8_t mask) 4012 | { 4013 | // xref 18fa 4014 | uint8_t snd = m.soundPort3; 4015 | snd |= mask; 4016 | m.soundPort3 = snd; 4017 | write_port(3, snd); 4018 | } 4019 | 4020 | // Init all the aliens for P2 4021 | static void InitAliensP2() 4022 | { 4023 | // xref 1904 4024 | InitAliensSub(u16_to_ptr(P2_ADDR)); 4025 | } 4026 | 4027 | // Called from the main thread to do some miscellaneous tasks 4028 | // a) Player shot collision response 4029 | // b) Detecting and handling the alien rack bumping the screen edges 4030 | static void PlyrShotAndBump() 4031 | { 4032 | // xref 190a 4033 | PlayerShotHit(); 4034 | RackBump(); 4035 | } 4036 | 4037 | // Return pointer to current player's alive status 4038 | static uint8_t* CurPlyAlive() 4039 | { 4040 | // xref 1910 4041 | return (m.playerDataMSB & 0x1 ? &m.playerStates.l : &m.playerStates.h); 4042 | } 4043 | 4044 | // Draw the score text labels at the top of the screen 4045 | static void DrawScoreHead() 4046 | { 4047 | // xref 191a 4048 | PrintMessage(xytosc(0,240), m.MSG__SCORE_1__HI_SCORE_SCORE_2__, 28); 4049 | } 4050 | 4051 | // Draw the score for P1 4052 | static void PrintP1Score() 4053 | { 4054 | // xref 1925 4055 | DrawScore(&m.P1Scor.l); 4056 | } 4057 | 4058 | // Draw the score for P2 4059 | static void PrintP2Score() 4060 | { 4061 | // xref 192b 4062 | DrawScore(&m.P2Scor.l); 4063 | } 4064 | 4065 | // Draw the score using the given descriptor in iter 4066 | static void DrawScore(uint8_t* iter) 4067 | { 4068 | // xref 1931 4069 | 4070 | Word pos; 4071 | Word val; 4072 | 4073 | val.l = *iter++; 4074 | val.h = *iter++; 4075 | pos.l = *iter++; 4076 | pos.h = *iter++; 4077 | 4078 | Print4Digits(pos, val); 4079 | } 4080 | 4081 | // Draw the credit text label at the bottom right 4082 | static void PrintCreditLabel() 4083 | { 4084 | // xref 193c 4085 | PrintMessage(xytosc(136,8), m.MSG_CREDIT_, 7); 4086 | } 4087 | 4088 | // Draw the number of credits at the bottom right 4089 | static void DrawNumCredits() 4090 | { 4091 | // xref 1947 4092 | DrawHexByte(xytosc(192,8), m.numCoins); 4093 | } 4094 | 4095 | // Draw the high score 4096 | static void PrintHiScore() 4097 | { 4098 | // xref 1950 4099 | DrawScore(&m.HiScor.l); 4100 | } 4101 | 4102 | // Clear the screen and draw all the text surrounding the playfield 4103 | static void DrawStatus() 4104 | { 4105 | // xref 1956 4106 | ClearScreen(); 4107 | DrawScoreHead(); 4108 | 4109 | PrintP1Score(); 4110 | PrintP2Score(); 4111 | 4112 | PrintHiScore(); 4113 | 4114 | PrintCreditLabel(); 4115 | DrawNumCredits(); 4116 | 4117 | // Midway patched this out 4118 | // PrintTaitoCorporation(); 4119 | } 4120 | 4121 | // Prints "* TAITO CORPORATION *" at the bottom of the screen 4122 | // This is a bit of dead code due to the patching out in DrawStatus() 4123 | static void PrintTaitoCorporation() 4124 | { 4125 | // xref 198b 4126 | PrintMessage(xytosc(32, 24), u16_to_ptr(0x19be), 0x13); 4127 | } 4128 | 4129 | // Called during the game demo to check if player has entered the correct 4130 | // button combos to display the easter egg, "TAITO COP" 4131 | static void CheckHiddenMes() 4132 | { 4133 | // xref 199a 4134 | uint8_t a = 0; 4135 | 4136 | if (m.hidMessSeq == 0) 4137 | { 4138 | a = read_port(1); 4139 | a &= 0x76; 4140 | a -= 0x72; 4141 | 4142 | if (a) 4143 | return; 4144 | 4145 | m.hidMessSeq = 1; 4146 | } 4147 | 4148 | a = read_port(1); 4149 | a &= 0x76; 4150 | a -= 0x34; 4151 | 4152 | if (a) 4153 | return; 4154 | 4155 | PrintMessage(xytosc(80,216), m.MSG_TAITO_COP, 9); 4156 | } 4157 | 4158 | // Allow game related tasks in interrupt routines 4159 | static void EnableGameTasks() 4160 | { 4161 | // xref 19d1 4162 | m.gameTasksRunning = 1; 4163 | } 4164 | 4165 | // Disallow game related tasks in interrupt routines 4166 | static void DsableGameTasks() 4167 | { 4168 | // xref 19d7 4169 | m.gameTasksRunning = 0; 4170 | } 4171 | 4172 | // Use a mask to turn off sounds in port 3 4173 | static void SoundBits3Off(uint8_t mask) 4174 | { 4175 | // xref 19dc 4176 | uint8_t snd = m.soundPort3; 4177 | snd &= mask; 4178 | m.soundPort3 = snd; 4179 | write_port(3, snd); 4180 | } 4181 | 4182 | // Draw the sprites representing the number of lives (0 based) remaining at the bottom left 4183 | static void DrawNumShips(uint8_t n) 4184 | { 4185 | // xref 19e6 4186 | SprDesc desc; 4187 | 4188 | desc.sc = xytosc(24,8); 4189 | desc.spr = ptr_to_word(m.PLAYER_SPRITES); 4190 | desc.n = 16; 4191 | 4192 | for (int i = 0; i < n; ++i) 4193 | desc.sc = DrawSimpSprite(&desc); 4194 | 4195 | DrawNumShipsSub(desc.sc); 4196 | } 4197 | 4198 | // Clears the space to the right of the ship sprites (used to remove lives) 4199 | static void DrawNumShipsSub(Word pos) 4200 | { 4201 | // xref 19fa 4202 | // Clear up to x = 136 (start of credit label) 4203 | 4204 | do 4205 | { 4206 | pos = ClearSmallSprite(pos, 16, 0); 4207 | } while ((pos.x != 0x35)); 4208 | } 4209 | 4210 | // Returns true if given obj is positioned on the half of screen that is not currently being drawn. 4211 | // Used to control which interrupt draws a particular game object. 4212 | // This prevents flicker. 4213 | static uint8_t CompXrToBeam(uint8_t* posaddr) 4214 | { 4215 | // xref 1a06 4216 | uint8_t b = m.vblankStatus; 4217 | uint8_t a = *posaddr & XR_MID; 4218 | 4219 | return a == b; 4220 | } 4221 | 4222 | // memcpy 4223 | static void BlockCopy(void *_dest, void *_src, size_t n) 4224 | { 4225 | // xref 1a32 4226 | 4227 | uint8_t* dest = (uint8_t*) (_dest); 4228 | uint8_t* src = (uint8_t*) (_src); 4229 | 4230 | for (size_t i = 0; i < n; ++i) 4231 | *dest++ = *src++; 4232 | } 4233 | 4234 | // Return a copy of a Sprite Descriptor from src 4235 | static SprDesc ReadDesc(SprDesc* src) 4236 | { 4237 | // xref 1a3b 4238 | SprDesc desc; 4239 | desc.spr.l = src->spr.l; 4240 | desc.spr.h = src->spr.h; 4241 | 4242 | desc.pos.l = src->pos.l; 4243 | desc.pos.h = src->pos.h; 4244 | 4245 | desc.n = src->n; 4246 | return desc; 4247 | } 4248 | 4249 | // Convert a pixel pos to a screen address 4250 | // Pixel positions in memory are pre-offset by +32 pixels, meaning that when 4251 | // they are converted to a screen coordinate, 0x400 has already been added. 4252 | // Hence the or with 0x2000 below, instead of +0x2400 4253 | // See xpix() 4254 | static Word ConvToScr(Word pos) 4255 | { 4256 | // xref 1a47 4257 | return u16_to_word((pos.u16 >> 3) | 0x2000); 4258 | } 4259 | 4260 | // bzero the 7168 bytes of vram 4261 | static void ClearScreen() 4262 | { 4263 | // xref 1a5c 4264 | uint8_t* screen = m.vram; 4265 | size_t n = 7168; 4266 | 4267 | for (size_t i = 0; i < n; ++i) 4268 | screen[i] = 0; 4269 | } 4270 | 4271 | // CopyShields() subroutine. 4272 | // cursor - used to iterate through the screen and the player buffer 4273 | // spr_size - contains the number of rows and bytes per row of the sprite 4274 | // dir=0 - Copy a shield from the buffer to the screen 4275 | // dir=1 - Copy a shield from the screen to the buffer 4276 | static void CopyShieldBuffer(ShieldBufferCursor *cursor, Word spr_size, uint8_t dir) 4277 | { 4278 | // xref 1a69 4279 | uint8_t nr = spr_size.h; 4280 | uint8_t nb = spr_size.l; 4281 | 4282 | for (int i = 0; i < nr; ++i, cursor->sc.u16 += xysc(1,0)) 4283 | { 4284 | uint8_t* screen = word_to_ptr(cursor->sc); 4285 | 4286 | for (int j = 0; j < nb; ++j) 4287 | { 4288 | if (dir == 0) 4289 | *screen = *cursor->iter | *screen; 4290 | else 4291 | *cursor->iter = *screen; 4292 | 4293 | cursor->iter++; 4294 | screen++; 4295 | } 4296 | } 4297 | } 4298 | 4299 | // Take a life away from the player, and update the indicators 4300 | static void RemoveShip() 4301 | { 4302 | // xref 1a7f 4303 | uint8_t* nships_ptr; 4304 | uint8_t num = GetNumberOfShips(&nships_ptr); 4305 | 4306 | if (num == 0) 4307 | return; 4308 | 4309 | *nships_ptr = num-1; 4310 | 4311 | // The sprite indicator is 0 based. 4312 | DrawNumShips(num-1); 4313 | 4314 | // The text indicator is 1 based. 4315 | DrawHexByteSub(xytosc(8,8), num & 0xf); 4316 | } 4317 | 4318 | // Print the numeric lives indicator at the bottom left 4319 | static void PrintNumShips(uint8_t num) 4320 | { 4321 | // xref 1a8b 4322 | DrawChar(xytosc(8,8), (num & 0x0f) + 0x1a); 4323 | } 4324 | 4325 | // GAMECODE FINISH 4326 | 4327 | -------------------------------------------------------------------------------- /si78c_proto.h: -------------------------------------------------------------------------------- 1 | static void do_logprintf(const char *file, unsigned line, const char* format, ...); 2 | int main(int argc, char **argv); 3 | static void input(); 4 | static void init_renderer(); 5 | static void fini_renderer(); 6 | static void render(); 7 | static void loop_core(int *credit); 8 | static void init_game(); 9 | static void fini_game(); 10 | static void init_threads(YieldReason entry_point); 11 | static void run_main_ctx(YieldReason entry); 12 | static void run_int_ctx(); 13 | static unsigned checksum(Mem *m); 14 | static void rom_load(void *mem, const char* name, size_t offset, size_t len); 15 | static void load_rom(void *mem); 16 | static int execute(int allowed); 17 | static void switch_to(ucontext_t *to); 18 | static void co_switch(ucontext_t *prev, ucontext_t *next); 19 | static void timeslice(); 20 | static void yield(YieldReason reason); 21 | static uint8_t get_input(int64_t ticks, uint8_t port); 22 | static uint8_t read_port(uint8_t port); 23 | static void write_port(uint16_t port, uint8_t v); 24 | static void enable_interrupts(); 25 | static void irq(uint8_t v); 26 | static int interrupted(); 27 | static void fatalerror(const char* format, ...); 28 | static inline Word u16_to_word(uint16_t u); 29 | static inline Word u8_u8_to_word(uint8_t h, uint8_t l); 30 | static inline uint16_t ptr_to_u16(uint8_t *ptr); 31 | static inline Word ptr_to_word(uint8_t *ptr); 32 | static inline uint8_t* u16_to_ptr(uint16_t u); 33 | static inline uint8_t* word_to_ptr(Word w); 34 | static int is_godmode(); 35 | static uint8_t* rompos(uint8_t* ram); 36 | static uint8_t bcd_add(uint8_t bcd, uint8_t a, uint8_t *carry); 37 | static void do_logprintf(const char *file, unsigned line, const char* format, ...); 38 | static void DebugMessage(Word sc, uint8_t* msg, uint8_t n); 39 | static void reset(); 40 | static void midscreen_int(); 41 | static void vblank_int(); 42 | static void vblank_coins(); 43 | static void InitRack(); 44 | static void InitAlienRacks(); 45 | static void DrawAlien(); 46 | static void CursorNextAlien(); 47 | static Word GetAlienCoords(uint8_t *rowout, uint8_t k); 48 | static void InitAliensSub(uint8_t* dest); 49 | static void InitAliensP1(); 50 | static void DrawBottomLine(); 51 | static uint8_t AddDelta(uint8_t* vecptr, uint8_t c); 52 | static void RestoreFromROM(uint8_t* addr, size_t n); 53 | static void CopyRomtoRam(); 54 | static void CopyRAMMirror(); 55 | static void DrawShieldP1(); 56 | static void DrawShieldP2(); 57 | static void DrawShieldCommon(uint8_t* dest); 58 | static void RememberShieldsP1(); 59 | static void RememberShieldsP2(); 60 | static void RestoreShieldsP2(); 61 | static void RestoreShieldsP1(); 62 | static void CopyShields(uint8_t dir, uint8_t* sprbuf); 63 | static void RunGameObjs(uint8_t* ptr); 64 | static void GameObj0(uint8_t* unused); 65 | static void HandleBlowingUpPlayer(uint8_t anim); 66 | static void player_death(int invaded); 67 | static int player_death_sub(int invaded); 68 | static void DrawPlayerShot(int op); 69 | static void GameObj1(uint8_t* unused); 70 | static SprDesc ReadPlyShot(); 71 | static void GameObj2(uint8_t* unused1); 72 | static void GameObj3(uint8_t* unused); 73 | static void ProcessSquigglyShot(); 74 | static void ToShotStruct(AShot* src, uint8_t picend); 75 | static void FromShotStruct(AShot* dest); 76 | static void HandleAlienShot(); 77 | static int DoHandleAlienShotMove(); 78 | static void HandleAlienShotMove(); 79 | static uint8_t FindInColumn(uint8_t *out, uint8_t col); 80 | static void ShotBlowingUp(); 81 | static void DrawAlienShot(); 82 | static void EraseAlienShot(); 83 | static void GameObj4(uint8_t* unused); 84 | static void RemoveSaucer(); 85 | static SprDesc GetSaucerInfo(); 86 | static void DrawPlayer(); 87 | static void DrawSaucer(); 88 | static void WaitForStart(); 89 | static void NewGame(uint8_t is2p, uint8_t cost, int skip); 90 | static uint8_t* GetRefAlienInfo(uint8_t *dxr, Word *pos); 91 | static uint8_t* GetAlRefPtr(); 92 | static void PromptPlayer(); 93 | static uint8_t GetShipsPerCred(); 94 | static void SpeedShots(); 95 | static void PrintMessage(Word sc, uint8_t* msg, size_t n); 96 | static Word DrawChar(Word sc, uint8_t c); 97 | static void TimeToSaucer(); 98 | static uint8_t GetNumberOfShips(uint8_t* *ptr); 99 | static void CheckAndHandleExtraShipAward(); 100 | static uint8_t* AlienScoreValue(uint8_t row); 101 | static void AdjustScore(); 102 | static void Print4Digits(Word sc, Word val); 103 | static Word DrawHexByte(Word sc, uint8_t c); 104 | static Word DrawHexByteSub(Word sc, uint8_t c); 105 | static uint8_t* GetScoreDescriptor(); 106 | static void ClearPlayField(); 107 | static void HandleEndOfTurn(); 108 | static void HandleEndOfTurnSub(); 109 | static uint8_t IsPlayerAlive(); 110 | static void ScoreForAlien(uint8_t row); 111 | static void Animate(); 112 | static Word PrintMessageDel(Word sc, uint8_t* str, uint8_t n); 113 | static void SplashSquiggly(); 114 | static void OneSecDelay(); 115 | static void TwoSecDelay(); 116 | static void SplashDemo(); 117 | static void ISRSplTasks(); 118 | static void MessageToCenterOfScreen(uint8_t* str); 119 | static void WaitOnDelay(uint8_t n); 120 | static void IniSplashAni(uint8_t* src); 121 | static uint8_t CheckPlyrShotAndBump(); 122 | static void EraseSimpleSprite(Word pos, uint8_t n); 123 | static Word DrawSimpSprite(SprDesc *desc); 124 | static Word CnvtPixNumber(Word pos); 125 | static void DrawShiftedSprite(struct SprDesc *desc); 126 | static void EraseShifted(struct SprDesc *desc); 127 | static void DrawSprCollision(struct SprDesc *desc); 128 | static void DrawSprite(struct SprDesc *desc); 129 | static void DrawSpriteGeneric(struct SprDesc *desc, int op); 130 | static Word ClearSmallSprite(Word sc, uint8_t n, uint8_t v); 131 | static void PlayerShotHit(); 132 | static uint8_t Cnt16s(uint8_t *v, uint8_t tgt); 133 | static Word FindRow(uint8_t y); 134 | static Word FindColumn(uint8_t x); 135 | static uint8_t* GetAlienStatPtr(uint8_t row, uint8_t col); 136 | static void RackBump(); 137 | static uint8_t RackBumpEdge(Word sc); 138 | static void CountAliens(); 139 | static uint8_t* GetPlayerDataPtr(); 140 | static void PlrFireOrDemo(); 141 | static void GameOver(); 142 | static void on_invaded(); 143 | static void AShotReloadRate(); 144 | static void ShotSound(); 145 | static void TimeFleetSound(); 146 | static void DisableFleetSound(); 147 | static void SetSoundWithoutFleet(uint8_t v); 148 | static uint8_t FleetDelayExShip(); 149 | static uint8_t ReadInputs(); 150 | static void CheckHandleTilt(); 151 | static void on_tilt(); 152 | static void CtrlSaucerSound(); 153 | static void DrawAdvTable(); 154 | static void Draw16ByteSprite(Word sc, uint8_t* sprite); 155 | static void PrintMessageAdv(Word sc, uint8_t* msg); 156 | static int ReadPriStruct(PriCursor *pri); 157 | static void SplashSprite(); 158 | static void AnimateShootingSplashAlien(); 159 | static void main_init(int skip); 160 | static uint8_t* GetOtherPlayerAliveFlag(); 161 | static void SoundBits3On(uint8_t mask); 162 | static void InitAliensP2(); 163 | static void PlyrShotAndBump(); 164 | static uint8_t* CurPlyAlive(); 165 | static void DrawScoreHead(); 166 | static void PrintP1Score(); 167 | static void PrintP2Score(); 168 | static void DrawScore(uint8_t* iter); 169 | static void PrintCreditLabel(); 170 | static void DrawNumCredits(); 171 | static void PrintHiScore(); 172 | static void DrawStatus(); 173 | static void PrintTaitoCorporation(); 174 | static void CheckHiddenMes(); 175 | static void EnableGameTasks(); 176 | static void DsableGameTasks(); 177 | static void SoundBits3Off(uint8_t mask); 178 | static void DrawNumShips(uint8_t n); 179 | static void DrawNumShipsSub(Word pos); 180 | static uint8_t CompXrToBeam(uint8_t* posaddr); 181 | static void BlockCopy(void *_dest, void *_src, size_t n); 182 | static SprDesc ReadDesc(SprDesc* src); 183 | static Word ConvToScr(Word pos); 184 | static void ClearScreen(); 185 | static void CopyShieldBuffer(ShieldBufferCursor *cursor, Word spr_size, uint8_t dir); 186 | static void RemoveShip(); 187 | static void PrintNumShips(uint8_t num); 188 | --------------------------------------------------------------------------------