si78c:用 C 语言实现的《太空侵略者》命令行游戏
OneFile
源码
//
// Space Invaders 1978 in C
// Jason McSweeney
#define _XOPEN_SOURCE
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <ucontext.h>
#include <stdint.h>
#include <assert.h>
#include <SDL.h>
#include <assert.h>
#include <string.h>
#define OP_BLEND 0
#define OP_ERASE 1
#define OP_COLLIDE 2
#define OP_BLIT 3
#define SHOT_ACTIVE 0x80
#define SHOT_BLOWUP 0x1
#define BEAM_VBLANK 0x80
#define BEAM_MIDDLE 0x00
#define XR_MID 0x80
#define DIP3_SHIPS1 0x1
#define DIP5_SHIPS2 0x2
#define DIP6_BONUS 0x8
#define DIP7_COININFO 0x80
#define TILT_BIT 0x4
#define COIN_BIT 0x1
#define INIT 1
#define PROMPT 2
#define SHIELDS 4
#define ROL_SHOT_PICEND 0xf9
#define PLU_SHOT_PICEND 0xed
#define SQU_SHOT_PICEND 0xdb
#define PLAYER_ADDR 0x2010
#define PLAYER_SIZE 16
#define PLAYER_SHOT_ADDR 0x2020
#define PLAYER_SHOT_DATA_ADDR 0x2025
#define PLAYER_SHOT_DATA_SIZE 7
#define ROLLING_SHOT_ADDR 0x2030
#define ROLLING_SHOT_SIZE 16
#define PLUNGER_SHOT_ADDR 0x2040
#define PLUNGER_SHOT_SIZE 16
#define SQUIGGLY_SHOT_ADDR 0x2050
#define SQUIGGLY_SHOT_SIZE 16
#define SAUCER_ADDR 0x2083
#define SAUCER_SIZE 10
#define P1_ADDR 0x2100
#define P2_ADDR 0x2200
#define SPLASH_DESC_ADDR 0x20c5
#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__
#error "This code is Little Endian only."
#endif
struct Word
{
union {
uint16_t u16;
struct {
uint8_t l;
uint8_t h;
};
struct {
uint8_t y;
uint8_t x;
};
};
} __attribute__ ((packed)); // 4
typedef struct Word Word;
struct SprDesc
{
Word spr;
union {
Word pos; // pixel position
Word sc; // screen address
};
uint8_t n;
} __attribute__ ((packed)); // 5
typedef struct SprDesc SprDesc;
struct GameObjHeader
{
uint8_t TimerMSB;
uint8_t TimerLSB;
uint8_t TimerExtra;
Word Handler;
} __attribute__ ((packed)); // 5
typedef struct GameObjHeader GameObjHeader;
struct AShot
{
uint8_t Status;
uint8_t StepCnt;
uint8_t Track;
Word CFir;
uint8_t BlowCnt;
SprDesc Desc;
} __attribute__ ((packed)); // 11
typedef struct AShot AShot;
struct Mem {
uint8_t pad_01[3063]; // 0x0000
uint8_t MSG_TAITO_COP[9]; // 0x0bf7
uint8_t pad_02[3601]; // 0x0c00
uint8_t soundDelayKey[16]; // 0x1a11
uint8_t soundDelayValue[16]; // 0x1a21
uint8_t pad_03[112]; // 0x1a31
uint8_t ShotReloadRate[5]; // 0x1aa1
uint8_t MSG_GAME_OVER__PLAYER___[20]; // 0x1aa6
uint8_t MSG_1_OR_2PLAYERS_BUTTON[20]; // 0x1aba
uint8_t pad_04; // 0x1ace
uint8_t MSG_ONLY_1PLAYER__BUTTON[20]; // 0x1acf
uint8_t pad_05; // 0x1ae3
uint8_t MSG__SCORE_1__HI_SCORE_SCORE_2__[28]; // 0x1ae4
uint8_t pad_06[112]; // 0x1b00
uint8_t MSG_PLAY_PLAYER_1_[14]; // 0x1b70
uint8_t pad_07[66]; // 0x1b7e
uint8_t SPLASH_SHOT_OBJDATA[16]; // 0x1bc0
uint8_t pad_08[144]; // 0x1bd0
uint8_t PLAYER_SPRITES[48]; // 0x1c60
uint8_t pad_09[9]; // 0x1c90
uint8_t MSG__10_POINTS[10]; // 0x1c99
uint8_t MSG__SCORE_ADVANCE_TABLE_[21]; // 0x1ca3
uint8_t AReloadScoreTab[4]; // 0x1cb8
uint8_t MSG_TILT[4]; // 0x1cbc
uint8_t pad_10[28]; // 0x1cc0
uint8_t AlienShotExplodingSprite[6]; // 0x1cdc
uint8_t pad_11[24]; // 0x1ce2
uint8_t MSG_PLAY2[5]; // 0x1cfa
uint8_t pad_12[33]; // 0x1cff
uint8_t SHIELD_SPRITE[44]; // 0x1d20
uint8_t SauScrValueTab[4]; // 0x1d4c
uint8_t SauScrStrTab[4]; // 0x1d50
uint8_t pad_13[40]; // 0x1d54
uint8_t SpriteSaucerExp[24]; // 0x1d7c
uint8_t MSG__50[3]; // 0x1d94
uint8_t MSG_100[3]; // 0x1d97
uint8_t MSG_150[3]; // 0x1d9a
uint8_t MSG_300[3]; // 0x1d9d
uint8_t AlienScores[3]; // 0x1da0
uint8_t AlienStartTable[8]; // 0x1da3
uint8_t MSG_PLAY[4]; // 0x1dab
uint8_t MSG_SPACE__INVADERS[15]; // 0x1daf
uint8_t SCORE_ADV_SPRITE_LIST[17]; // 0x1dbe
uint8_t SCORE_ADV_MSG_LIST[17]; // 0x1dcf
uint8_t MSG____MYSTERY[10]; // 0x1de0
uint8_t MSG__30_POINTS[10]; // 0x1dea
uint8_t MSG__20_POINTS[10]; // 0x1df4
uint8_t pad_14[338]; // 0x1dfe
uint8_t MSG__1_OR_2_PLAYERS___[18]; // 0x1f50
uint8_t pad_15[46]; // 0x1f62
uint8_t MSG_INSERT__COIN[12]; // 0x1f90
uint8_t CREDIT_TABLE[4]; // 0x1f9c
uint8_t CREDIT_TABLE_COINS[9]; // 0x1fa0
uint8_t MSG_CREDIT_[7]; // 0x1fa9
uint8_t pad_16[67]; // 0x1fb0
uint8_t MSG_PUSH[4]; // 0x1ff3
uint8_t pad_17[9]; // 0x1ff7
// start of ram mirror
uint8_t waitOnDraw; // 0x2000
uint8_t pad_18; // 0x2001
uint8_t alienIsExploding; // 0x2002
uint8_t expAlienTimer; // 0x2003
uint8_t alienRow; // 0x2004
uint8_t alienFrame; // 0x2005
uint8_t alienCurIndex; // 0x2006
Word refAlienDelta; // 0x2007
Word refAlienPos; // 0x2009
Word alienPos; // 0x200b
uint8_t rackDirection; // 0x200d
uint8_t rackDownDelta; // 0x200e
uint8_t pad_19; // 0x200f
GameObjHeader playerHeader; // 0x2010
uint8_t playerAlive; // 0x2015
uint8_t expAnimateTimer; // 0x2016
uint8_t expAnimateCnt; // 0x2017
SprDesc playerDesc; // 0x2018
uint8_t nextDemoCmd; // 0x201d
uint8_t hidMessSeq; // 0x201e
uint8_t pad_20; // 0x201f
GameObjHeader plyrShotHeader; // 0x2020
uint8_t plyrShotStatus; // 0x2025
uint8_t blowUpTimer; // 0x2026
SprDesc playerShotDesc; // 0x2027
uint8_t shotDeltaYr; // 0x202c
uint8_t fireBounce; // 0x202d
uint8_t pad_21[2]; // 0x202e
GameObjHeader rolShotHeader; // 0x2030
AShot rolShotData; // 0x2035
GameObjHeader pluShotHeader; // 0x2040
AShot pluShotData; // 0x2045
GameObjHeader squShotHeader; // 0x2050
AShot squShotData; // 0x2055
uint8_t pad_22; // 0x2060
uint8_t collision; // 0x2061
SprDesc expAlien; // 0x2062
uint8_t playerDataMSB; // 0x2067
uint8_t playerOK; // 0x2068
uint8_t enableAlienFire; // 0x2069
uint8_t alienFireDelay; // 0x206a
uint8_t pad_23; // 0x206b
uint8_t temp206C; // 0x206c
uint8_t invaded; // 0x206d
uint8_t skipPlunger; // 0x206e
uint8_t pad_24; // 0x206f
uint8_t otherShot1; // 0x2070
uint8_t otherShot2; // 0x2071
uint8_t vblankStatus; // 0x2072
AShot aShot; // 0x2073
uint8_t alienShotDelta; // 0x207e
uint8_t shotPicEnd; // 0x207f
uint8_t shotSync; // 0x2080
uint8_t tmp2081; // 0x2081
uint8_t numAliens; // 0x2082
uint8_t saucerStart; // 0x2083
uint8_t saucerActive; // 0x2084
uint8_t saucerHit; // 0x2085
uint8_t saucerHitTime; // 0x2086
SprDesc saucerDesc; // 0x2087
uint8_t saucerDXr; // 0x208c
Word sauScore; // 0x208d
Word shotCount; // 0x208f
Word saucerTimer; // 0x2091
uint8_t waitStartLoop; // 0x2093
uint8_t soundPort3; // 0x2094
uint8_t changeFleetSnd; // 0x2095
uint8_t fleetSndCnt; // 0x2096
uint8_t fleetSndReload; // 0x2097
uint8_t soundPort5; // 0x2098
uint8_t extraHold; // 0x2099
uint8_t tilt; // 0x209a
uint8_t fleetSndHold; // 0x209b
uint8_t pad_25[36]; // 0x209c
// end of partial ram restore at 0x20c0
uint8_t isrDelay; // 0x20c0
uint8_t isrSplashTask; // 0x20c1
uint8_t splashAnForm; // 0x20c2
Word splashDelta; // 0x20c3
Word splashPos; // 0x20c5
Word splashPic; // 0x20c7
uint8_t splashPicSize; // 0x20c9
uint8_t splashTargetX; // 0x20ca
uint8_t splashReached; // 0x20cb
Word splashImRest; // 0x20cc
uint8_t twoPlayers; // 0x20ce
uint8_t aShotReloadRate; // 0x20cf
uint8_t pad_26[21]; // 0x20d0
Word playerExtras; // 0x20e5
Word playerStates; // 0x20e7
uint8_t gameTasksRunning; // 0x20e9
uint8_t coinSwitch; // 0x20ea
uint8_t numCoins; // 0x20eb
uint8_t splashAnimate; // 0x20ec
Word demoCmdPtr; // 0x20ed
uint8_t gameMode; // 0x20ef
uint8_t pad_27; // 0x20f0
uint8_t adjustScore; // 0x20f1
Word scoreDelta; // 0x20f2
Word HiScor; // 0x20f4
uint8_t pad_28[2]; // 0x20f6
Word P1Scor; // 0x20f8
uint8_t pad_29[2]; // 0x20fa
Word P2Scor; // 0x20fc
uint8_t pad_30[68]; // 0x20fe
// end of ram mirror at 0x2100
uint8_t p1ShieldBuffer[176]; // 0x2142
uint8_t pad_31[9]; // 0x21f2
uint8_t p1RefAlienDX; // 0x21fb
Word p1RefAlienPos; // 0x21fc
uint8_t p1RackCnt; // 0x21fe
uint8_t p1ShipsRem; // 0x21ff
uint8_t pad_32[66]; // 0x2200
uint8_t p2ShieldBuffer[176]; // 0x2242
uint8_t pad_33[9]; // 0x22f2
uint8_t p2RefAlienDX; // 0x22fb
Word p2RefAlienPos; // 0x22fc
uint8_t p2RackCnt; // 0x22fe
uint8_t p2ShipsRem; // 0x22ff
uint8_t pad_34[256]; // 0x2300
uint8_t vram[7168]; // 0x2400
// Technically the region below is supposed to be a mirror, but AFAICT,
// that property is not used by the SI code.
//
// The 'PLAy' animation does do some oob writes during DrawSprite,
// which effectively do nothing because they end up trying to write to ROM
//
// So, as a catchall, we just reserve this area up to the end of the address space.
uint8_t oob[49152]; // 0x4000
} __attribute ((packed));
typedef struct Mem Mem;
typedef struct PriCursor
{
uint8_t* src;
Word sc;
uint8_t* obj;
} PriCursor;
typedef struct ShieldBufferCursor
{
Word sc;
uint8_t* iter;
} ShieldBufferCursor;
typedef enum YieldReason
{
YIELD_INIT = 0,
YIELD_TIMESLICE,
YIELD_INTFIN,
YIELD_WAIT_FOR_START,
YIELD_PLAYER_DEATH,
YIELD_INVADED,
YIELD_TILT,
YIELD_UNKNOWN,
} YieldReason;
enum Keys {
KEYS_LEFT = 1,
KEYS_RIGHT = 2,
KEYS_START = 4,
KEYS_START2 = 8,
KEYS_FIRE = 16,
KEYS_COIN = 32,
KEYS_TILT = 64,
KEYS_DIP6 = 128,
KEYS_DIP7 = 256,
KEYS_SPECIAL1 = 512,
KEYS_SPECIAL2 = 1024,
KEYS_QUIT = 2048
};
#define KEY_LIST \
KEY_MAP('a', KEYS_LEFT); \
KEY_MAP('d', KEYS_RIGHT); \
KEY_MAP('1', KEYS_START); \
KEY_MAP('2', KEYS_START2); \
KEY_MAP('j', KEYS_FIRE); \
KEY_MAP('5', KEYS_COIN); \
KEY_MAP('t', KEYS_TILT); \
KEY_MAP('6', KEYS_DIP6); \
KEY_MAP('7', KEYS_DIP7); \
KEY_MAP('z', KEYS_SPECIAL1); \
KEY_MAP('x', KEYS_SPECIAL2); \
KEY_MAP(SDLK_ESCAPE, KEYS_QUIT);
#define TRUE 1
#define FALSE 0
static void do_logprintf(const char *file, unsigned line, const char* format, ...);
#define logprintf(...) { do_logprintf(__FILE__, __LINE__, __VA_ARGS__); }
#include "si78c_proto.h"
// Coordinate Systems
// ------------------
//
// Natural Units
// -------------
//
// For readability, this codebase uses the following coordinate system
// where possible.
//
// The origin is at the bottom left corner.
//
// X goes +ve towards the rhs of the screen.
// Y goes +ve towards the top of the screen.
//
// (0,256)
// ^
// |
// y |
// |
// |----------> (224,0)
// (0,0) x
//
// Game Units
// -------------
//
// The game uses two different coordinate systems, both of which
// fit into a 16-bit word, and come with an offset.
//
// pix - Pixel positions between 0x2000 and 0xffff
// sc - Screen RAM addresses between 0x2400 and 0x3fff
//
// pix coordinates are used to move and draw objects that require per pixel shifting,
// like the aliens and the bullets.
//
// sc coordinates are used for drawing text and other simple sprites, and also when
// blitting sprites after they have been shifted.
//
// The following macros are used to convert between natural units and game units.
#define xysc(x, y) ((x)*32 + (y)/8)
#define xytosc(x, y) u16_to_word(0x2400 + xysc((x),(y)))
#define xpix(x) ((x)+32)
#define xytopix(x, y) u16_to_word((xpix((x)) << 8) | (y))
#define STACK_SIZE 65536
#define CRED1 17152
#define CRED2 16384
static Mem m;
static uint8_t *rawmem = (uint8_t*) &m;
static int64_t ticks;
static int im;
static int irq_state;
static int irq_vector;
static uint16_t shift_data;
static uint8_t shift_count;
static ucontext_t frontend_ctx;
static ucontext_t main_ctx;
static ucontext_t int_ctx;
static ucontext_t *prev_ctx;
static ucontext_t *curr_ctx;
static uint8_t main_ctx_stack[STACK_SIZE];
static uint8_t int_ctx_stack[STACK_SIZE];
static YieldReason yield_reason;
static SDL_Window *window;
static SDL_Renderer *renderer;
static const int renderscale = 2;
static uint64_t keystate;
static int exited;
static uint8_t port1;
static uint8_t port2;
int main(int argc, char **argv)
{
init_renderer();
init_game();
int credit = 0;
size_t frame = -1;
while (1)
{
frame++;
input();
if (exited) break;
// preserves timing compatibility with MAME
if (frame == 1)
credit--;
// up to mid
credit += CRED1;
loop_core(&credit);
irq(0xcf);
// up to vblank
credit += CRED2;
loop_core(&credit);
irq(0xd7);
render();
}
fini_game();
fini_renderer();
return 0;
}
static void input()
{
SDL_Event event_buffer[64];
size_t num = 0;
while (num < 64)
{
int has = SDL_PollEvent(&event_buffer[num]);
if (!has) break;
num++;
}
for (size_t i = 0; i < num; ++i)
{
SDL_Event e = event_buffer[i];
if (e.type == SDL_QUIT)
{
e.type = SDL_KEYDOWN;
e.key.keysym.sym = SDLK_ESCAPE;
}
if (! (e.type == SDL_KEYDOWN || e.type == SDL_KEYUP)) continue;
uint64_t mask = 0;
uint64_t f = e.type == SDL_KEYDOWN;
switch (e.key.keysym.sym)
{
#define KEY_MAP(x, y) case x: mask = y; break;
KEY_LIST
#undef KEY_MAP
}
keystate = (keystate & ~mask) | (-f & mask);
}
#define BIT(x) (!!(keystate & (x)))
port1 = (BIT(KEYS_RIGHT) << 6) |
(BIT(KEYS_LEFT) << 5) |
(BIT(KEYS_FIRE) << 4) |
(1 << 3) |
(BIT(KEYS_START) << 2) |
(BIT(KEYS_START2) << 1) |
(BIT(KEYS_COIN) << 0);
port2 = (BIT(KEYS_DIP7) << 7) |
(BIT(KEYS_RIGHT) << 6) |
(BIT(KEYS_LEFT) << 5) |
(BIT(KEYS_FIRE) << 4) |
(BIT(KEYS_DIP6) << 3) |
(BIT(KEYS_TILT) << 2);
exited = BIT(KEYS_QUIT);
}
static void init_renderer()
{
int rc = SDL_Init(SDL_INIT_EVERYTHING);
assert(rc == 0);
window = SDL_CreateWindow("si78c", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
224 * renderscale, 256 * renderscale, 0);
assert(window);
SDL_ShowCursor(SDL_DISABLE);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
assert(renderer);
}
static void fini_renderer()
{
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
}
static void render()
{
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
const uint8_t *iter = rawmem + 0x2400;
for (int y = 0; y < 224; ++y)
{
for (int xi = 0; xi < 32; ++xi)
{
uint8_t byte = *iter++;
for (int i = 0; i < 8; ++i)
{
int x = xi * 8 + i;
int on = (byte >> i) & 0x1;
if (on)
{
SDL_Rect rect;
rect.x = y * renderscale;
rect.y = (256 - x - 1) * renderscale;
rect.w = renderscale;
rect.h = renderscale;
SDL_RenderDrawRect(renderer, &rect);
}
}
}
}
SDL_RenderPresent(renderer);
}
static void loop_core(int *credit)
{
int allowed = 1;
while (*credit > 0)
*credit -= execute(allowed);
}
static void init_game()
{
assert(sizeof(m) == 0x10000);
load_rom(&m);
assert(checksum(&m) == 0x6dfbd7cc);
init_threads(YIELD_INIT);
}
static void fini_game()
{
}
static void init_threads(YieldReason entry_point)
{
int rc = getcontext(&main_ctx);
assert(rc == 0);
main_ctx.uc_stack.ss_sp = main_ctx_stack;
main_ctx.uc_stack.ss_size = STACK_SIZE;
main_ctx.uc_link = &frontend_ctx;
makecontext(&main_ctx, (void (*)()) run_main_ctx, 1, entry_point);
rc = getcontext(&int_ctx);
int_ctx.uc_stack.ss_sp = &int_ctx_stack;
int_ctx.uc_stack.ss_size = STACK_SIZE;
int_ctx.uc_link = &frontend_ctx;
makecontext(&int_ctx, run_int_ctx, 0);
prev_ctx = &main_ctx;
curr_ctx = &frontend_ctx;
}
static void run_main_ctx(YieldReason entry)
{
switch(entry)
{
case YIELD_INIT: reset(); break;
case YIELD_WAIT_FOR_START: WaitForStart(); break;
case YIELD_PLAYER_DEATH: player_death(0); break;
case YIELD_INVADED: on_invaded(); break;
case YIELD_TILT: on_tilt(); break;
default: assert(FALSE);
}
}
static void run_int_ctx()
{
while (1)
{
// 0xcf = RST 1 opcode (call 0x8)
// 0xd7 = RST 2 opcode (call 0x16)
if (irq_vector == 0xcf)
midscreen_int();
else if (irq_vector == 0xd7)
vblank_int();
enable_interrupts();
yield(YIELD_INTFIN);
}
}
static unsigned checksum(Mem *m)
{
assert(sizeof(*m) == 0x10000);
assert((uintptr_t) m % 4 == 0);
unsigned *ptr = (unsigned*) m;
size_t n = sizeof(*m) / 4;
unsigned sum = 0;
for (size_t i = 0; i < n; ++i)
sum += ptr[i];
return sum;
}
static void rom_load(void *mem, const char* name, size_t offset, size_t len)
{
char fbuf[256]; sprintf(fbuf, "inv1/%s", name);
FILE *romfile = fopen(fbuf, "r");
assert(romfile);
ssize_t rn = fread((char*) mem + offset, 1, len, romfile);
assert((size_t) rn == len);
fclose(romfile);
}
static void load_rom(void *mem)
{
rom_load(mem, "invaders.h", 0x0000, 0x0800);
rom_load(mem, "invaders.g", 0x0800, 0x0800);
rom_load(mem, "invaders.f", 0x1000, 0x0800);
rom_load(mem, "invaders.e", 0x1800, 0x0800);
}
static int execute(int allowed)
{
int64_t start = ticks;
ucontext_t *next = NULL;
switch (yield_reason)
{
case YIELD_INIT:
case YIELD_TIMESLICE:
next = prev_ctx;
break;
case YIELD_INTFIN:
next = &main_ctx;
break;
case YIELD_PLAYER_DEATH:
case YIELD_WAIT_FOR_START:
case YIELD_INVADED:
init_threads(yield_reason);
enable_interrupts();
next = &main_ctx;
break;
case YIELD_TILT:
init_threads(yield_reason);
next = &main_ctx;
break;
default:
assert(FALSE);
}
yield_reason = YIELD_UNKNOWN;
if (allowed && interrupted())
{
next = &int_ctx;
}
switch_to(next);
return ticks - start;
}
static void switch_to(ucontext_t *to)
{
co_switch(curr_ctx, to);
}
static void co_switch(ucontext_t *prev, ucontext_t *next)
{
prev_ctx = prev;
curr_ctx = next;
swapcontext(prev, next);
}
static void timeslice()
{
ticks += 30;
yield(YIELD_TIMESLICE);
}
static void yield(YieldReason reason)
{
yield_reason = reason;
switch_to(&frontend_ctx);
}
static uint8_t get_input(int64_t ticks, uint8_t port)
{
if (port == 1)
return port1;
if (port == 2)
return port2;
fatalerror("unknown port %d\n", port);
return 0;
}
static uint8_t read_port(uint8_t port)
{
if (port == 3)
return (shift_data << shift_count) >> 8;
uint8_t val = get_input(ticks, port);
return val;
}
static void write_port(uint16_t port, uint8_t v)
{
if (port == 2)
{
shift_count = v & 0x7;
}
else if (port == 4)
{
shift_data = (v << 8) | (shift_data >> 8);
}
timeslice();
}
static void enable_interrupts()
{
im = 1;
}
static void irq(uint8_t v)
{
irq_vector = v;
irq_state = 1;
}
static int interrupted()
{
// The two interrupts correspond to midscreen, and start of vblank.
// 0xcf = RST 1 opcode (call 0x8)
// 0xd7 = RST 2 opcode (call 0x16)
if (irq_state && im)
{
assert(irq_vector == 0xcf || irq_vector == 0xd7);
irq_state = 0;
im = 0;
return TRUE;
}
return FALSE;
}
static void fatalerror(const char* format, ...)
{
va_list ap;
va_start(ap, format);
vfprintf(stdout, format, ap);
va_end(ap);
fflush(stdout);
fflush(stderr);
exit(1);
}
static inline Word u16_to_word(uint16_t u)
{
Word w; w.u16 = u; return w;
}
static inline Word u8_u8_to_word(uint8_t h, uint8_t l)
{
return u16_to_word((h << 8) | l);
}
static inline uint16_t ptr_to_u16(uint8_t *ptr)
{
return (uint16_t) (ptr - (uint8_t*) &m);
}
static inline Word ptr_to_word(uint8_t *ptr)
{
return u16_to_word(ptr_to_u16(ptr));
}
static inline uint8_t* u16_to_ptr(uint16_t u)
{
return ((uint8_t*) &m) + u;
}
static inline uint8_t* word_to_ptr(Word w)
{
return u16_to_ptr(w.u16);
}
static int is_godmode()
{
uint16_t addr = 0x060f;
uint8_t nops[] = {0,0,0};
return memcmp(((uint8_t*) &m) + addr, nops, 3) == 0;
}
static uint8_t* rompos(uint8_t* ram)
{
return ram - 0x500;
}
static uint8_t bcd_add(uint8_t bcd, uint8_t a, uint8_t *carry)
{
// Add the given number to the given bcd value, as per ADI / ADDB etc
int q = bcd + a + *carry;
*carry = (q >> 8) & 0x1;
int aux = (bcd ^ q ^ a) & 0x10;
bcd = q;
// Adjust the result back into bcd as per DAA
uint8_t w = bcd;
if (aux || ((bcd & 0xf) > 9)) w += 6;
if ((*carry) || (bcd > 0x99)) w += 0x60;
*carry |= bcd > 0x99;
bcd = w;
return bcd;
}
static void do_logprintf(const char *file, unsigned line, const char* format, ...)
{
fprintf(stdout, "%s:%d: ", file, line);
va_list ap;
va_start(ap, format);
vfprintf(stdout, format, ap);
va_end(ap);
fflush(stdout);
}
static void DebugMessage(Word sc, uint8_t* msg, uint8_t n)
{
uint16_t raw = sc.u16 - 0x2400;
uint16_t x = raw / 32;
uint16_t y = (raw % 32) * 8;
static const char *alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789<> =*...............?......-";
char sbuf[256];
for (size_t i = 0; i < n; ++i)
sbuf[i] = alpha[msg[i]];
sbuf[n] = '\0';
logprintf("Print Message %04x %d (%d,%d) \"%s\"\n", ptr_to_word(msg).u16, n, x, y, sbuf);
}
// GAMECODE START
// The entry point on reset or power up
static void reset()
{
// xref 0000
main_init(0);
}
// Executed via interrupt when the beam hits the middle of the screen
static void midscreen_int()
{
// xref 0008
// xref 008c
m.vblankStatus = BEAM_MIDDLE;
if (m.gameTasksRunning == 0) return;
if (!m.gameMode && !(m.isrSplashTask & 0x1)) return;
// xref 00a5
// Run game objects but skip over first entry (player)
RunGameObjs(u16_to_ptr(PLAYER_SHOT_ADDR));
CursorNextAlien();
}
// Executed via interrupt when the beam hits the end of the screen
static void vblank_int()
{
// xref 0010
m.vblankStatus = BEAM_VBLANK;
m.isrDelay--;
CheckHandleTilt();
vblank_coins();
if (m.gameTasksRunning == 0)
return;
if (m.gameMode)
{
// xref 006f
TimeFleetSound();
m.shotSync = m.rolShotHeader.TimerExtra;
DrawAlien();
RunGameObjs(u16_to_ptr(PLAYER_ADDR));
TimeToSaucer();
return;
}
if (m.numCoins != 0)
{
// xref 005d
if (m.waitStartLoop) return;
m.waitStartLoop = 1;
yield(YIELD_WAIT_FOR_START);
assert(FALSE);
}
ISRSplTasks();
}
// Read coin input and handle debouncing
static void vblank_coins()
{
// xref 0020
if (read_port(1) & COIN_BIT)
{
// xref 0067
m.coinSwitch = 1; // Remember switch state for debounce
// xref 003f
// useless update
m.coinSwitch = 1;
return;
}
// xref 0026
// Skip registering the credit if prev and current states were both zero
if (m.coinSwitch == 0)
return;
uint8_t bcd = m.numCoins;
// Add credit if it won't cause a rollover
if (bcd != 0x99)
{
uint8_t carry = 0;
m.numCoins = bcd_add(bcd, 1, &carry);
DrawNumCredits();
}
m.coinSwitch = 0;
}
// Initialize the alien rack speed and position using the current player's alien rack data
static void InitRack()
{
// xref 00b1
uint8_t* al_ref_ptr = GetAlRefPtr();
Word refpos;
refpos.y = *(al_ref_ptr);
refpos.x = *(al_ref_ptr + 1);
m.refAlienPos = refpos;
m.alienPos = refpos;
uint8_t dxr = *(al_ref_ptr - 1);
if (dxr == 3)
--dxr;
m.refAlienDelta.x = dxr;
m.rackDirection = (dxr == (uint8_t) -2) ? 1 : 0;
}
// Set up P1 and P2 Rack data
static void InitAlienRacks()
{
// xref 00d7
// Added for si78c testing infrastructure
// In normal operation, dx is hardcoded to 2
uint8_t dx = ((uint8_t*) &m)[0x00d8];
m.p1RefAlienDX = dx;
m.p2RefAlienDX = dx;
// xref 08e4
if (m.twoPlayers)
return;
ClearSmallSprite(xytosc(168, 224), 32, 0);
}
// Draw the alien, called via vblank_int()
// Only one alien is ever drawn per frame, causing a ripple effect.
// This also somewhat determines the game speed.
//
// The alien rack is effectively moving at
// (2.0 / num_aliens) pixels per frame
//
// If 55 aliens are alive, then it will take almost one second to move all the aliens by 2 pixels.
// If 1 alien is alive, then it will only take one frame to move 2 pixels.
static void DrawAlien()
{
// xref 0100
if (m.alienIsExploding)
{
if (--m.expAlienTimer) return;
EraseSimpleSprite(m.expAlien.pos, 16);
m.plyrShotStatus = 4;
m.alienIsExploding = 0;
SoundBits3Off(0xf7);
return;
}
uint8_t ai = m.alienCurIndex;
uint8_t* alienptr = u16_to_ptr(m.playerDataMSB << 8 | ai);
if (*alienptr == 0)
{
// The alien is dead, so skip it and tell CursorNextAlien to advance.
m.waitOnDraw = 0;
return;
}
// Look up the correct alien sprite based on row and anim frame
uint8_t row = m.alienRow;
static const uint8_t mul[] = {0,0,16,16,32};
uint8_t* sprite = u16_to_ptr(0x1c00 + mul[row] + m.alienFrame * 48);
SprDesc desc;
desc.pos = m.alienPos;
desc.spr = ptr_to_word(sprite);
desc.n = 16;
DrawSprite(&desc);
m.waitOnDraw = 0; // This flag is synced with CursorNextAlien
}
// Find the next live alien to draw. Also detects whether rack has reached the bottom.
static void CursorNextAlien()
{
// xref 0141
if (m.playerOK == 0) return;
if (m.waitOnDraw != 0) return;
Word ai;
ai.h = m.playerDataMSB;
ai.l = m.alienCurIndex;
uint8_t movecnt = 0x02; // limits MoveRefAlien to be called once
// Advance the cursor until we find a live alien to draw.
// If the cursor reaches the end, move ref alien, flip anim and reset cursor to zero.
while (TRUE)
{
timeslice();
++ai.l;
// inlined MoveRefAlien
if (ai.l == 55)
{
if (--movecnt == 0) return;
m.alienCurIndex = 0;
ai.l = 0;
uint8_t dy = m.refAlienDelta.y;
m.refAlienDelta.y = 0;
AddDelta(&m.refAlienDelta.y, dy);
m.alienFrame = !m.alienFrame;
(void) (uint8_t) (m.playerDataMSB); // unused
}
// If we found live alien to draw, then break
if (*word_to_ptr(ai)) break;
}
m.alienCurIndex = ai.l;
uint8_t row = 0;
Word pixnum = GetAlienCoords(&row, ai.l);
m.alienPos = pixnum;
if (pixnum.y < 40)
{
// xref 1971
// kill the player due to invasion
m.invaded = 1;
yield(YIELD_INVADED);
assert(FALSE);
}
m.alienRow = row;
m.waitOnDraw = 1;
}
// Given alien index k, return alien position and row
static Word GetAlienCoords(uint8_t *rowout, uint8_t k)
{
// xref 017a
uint8_t row = k / 11;
uint8_t col = k % 11;
uint8_t y = m.refAlienPos.y + 16 * row;
uint8_t x = m.refAlienPos.x + 16 * col;
*rowout = row;
return u8_u8_to_word(x, y);
}
// Init all the aliens at dest to alive
static void InitAliensSub(uint8_t* dest)
{
// xref 01c0
for (int i = 0; i < 55; ++i)
*dest++ = 1;
}
// Init all the aliens for P1
static void InitAliensP1()
{
// xref 01c0
InitAliensSub(u16_to_ptr(P1_ADDR));
}
// Draw a one pixel (leftmost/bottommost in byte) stripe across the screen (224 pix wide)
// 16 pixels above the origin.
static void DrawBottomLine()
{
// xref 01cf
ClearSmallSprite(xytosc(0,16), 224, 1);
}
// Given four bytes at vecptr treat them as two vecs - dy dx, y x
// and do this:
//
// (y,x) += (c, dx)
//
// Used to move objects
static uint8_t AddDelta(uint8_t* vecptr, uint8_t c)
{
vecptr++; // skip dy
uint8_t dy = c;
uint8_t dx = *vecptr++;
uint8_t y = *vecptr + dy;
*vecptr++ = y;
uint8_t x = *vecptr + dx;
*vecptr++ = x;
return x;
}
// Restore into RAM mirror (addr, n) from ROM
static void RestoreFromROM(uint8_t* addr, size_t n)
{
BlockCopy(addr, rompos(addr), n);
}
// Restore the entire RAM mirror (256 bytes), used at startup
static void CopyRomtoRam()
{
// xref 01e6
RestoreFromROM(u16_to_ptr(0x2000), 256);
}
// Partially restore the RAM mirror (first 192 bytes)
// The last 64 bytes are managed elsewhere, and some are
// persistent across games (hi scores etc)
static void CopyRAMMirror()
{
// xref 01e4
RestoreFromROM(u16_to_ptr(0x2000), 192);
}
// Initialize P1 shields into off screen buffer
static void DrawShieldP1()
{
// xref 01ef
DrawShieldCommon(m.p1ShieldBuffer);
}
// Initialize P2 shields into off screen buffer
static void DrawShieldP2()
{
// xref 01f5
DrawShieldCommon(m.p2ShieldBuffer);
}
// Initialize shields into given buffer
static void DrawShieldCommon(uint8_t* dest)
{
// xref 01f8
size_t n = 44;
for (int i = 0; i < 4; ++i, dest += n)
BlockCopy(dest, m.SHIELD_SPRITE, n);
}
// Copy on screen shields into P1 off screen buffer to remember damage
static void RememberShieldsP1()
{
// xref 0209
CopyShields(1, m.p1ShieldBuffer);
}
// Copy on screen shields into P2 off screen buffer to remember damage
static void RememberShieldsP2()
{
// xref 020e
CopyShields(1, m.p2ShieldBuffer);
}
// Copy off screen shields from P2 buffer back to screen
static void RestoreShieldsP2()
{
// xref 0213
CopyShields(0, m.p2ShieldBuffer);
}
// Copy off screen shields from P1 buffer back to screen
static void RestoreShieldsP1()
{
// xref 021a
CopyShields(0, m.p1ShieldBuffer);
}
// Generic shield copy routine. Copy into or out of given buffer.
// dir = 0 - buffer to screen
// dir = 1 - screen to buffer
static void CopyShields(uint8_t dir, uint8_t* sprbuf)
{
// xref 021e
m.tmp2081 = dir;
Word sprsz = u8_u8_to_word(22, 2); // 22 rows, 2 bytes per row
ShieldBufferCursor cursor;
cursor.sc = xytosc(32, 48);
cursor.iter = sprbuf;
for (int i = 0; i < 4; ++i)
{
uint8_t unused = m.tmp2081;
(void) (unused);
CopyShieldBuffer(&cursor, sprsz, dir);
cursor.sc.u16 += xysc(23, 0);
}
}
// Do the generic (base class) handling for the 5 game objects.
// Checks the timer flags to see if object is ready to run, and if so
// finds the appropriate subclass handler and calls it.
static void RunGameObjs(uint8_t* ptr)
{
// xref 0248
for ( ; ; ptr += 16)
{
GameObjHeader* obj = (GameObjHeader*)(ptr);
uint8_t timer_hi = obj->TimerMSB;
// end of list check
if (timer_hi == 0xff) return;
// object skip check
if (timer_hi == 0xfe) continue;
uint8_t timer_lo = obj->TimerLSB;
// decrement timer if its not zero
if (timer_hi | timer_lo)
{
// xref 0277
// timers are big endian for some reason.
if (timer_lo == 0) --timer_hi; // decrement msb if necc
--timer_lo; // decrement lsb
obj->TimerLSB = timer_lo;
obj->TimerMSB = timer_hi;
continue;
}
uint8_t timer_extra = obj->TimerExtra;
if (timer_extra != 0)
{
// xref 0288
obj->TimerExtra--;
continue;
}
// The object is ready to run, so grab the handler and run it
uint16_t fnlo = obj->Handler.l;
uint16_t fnhi = obj->Handler.h;
uint16_t fn = fnhi << 8 | fnlo;
uint8_t* data = (ptr + sizeof(GameObjHeader));
switch(fn)
{
case 0x028e: GameObj0(data); break;
case 0x03bb: GameObj1(data); break;
case 0x0476: GameObj2(data); break;
case 0x04b6: GameObj3(data); break;
case 0x0682: GameObj4(data); break;
case 0x050e: ProcessSquigglyShot(); break; // splash anim
default:
assert(FALSE);
break;
}
}
}
// Handles player movement and rendering
// Unlike other game objects, this is only run during the vblank interrupt, and
// runs on each vblank interrupt
static void GameObj0(uint8_t* unused)
{
// xref 028e
uint8_t pstate = m.playerAlive;
if (pstate != 0xff) // 0xff means player is alive
{
HandleBlowingUpPlayer(pstate);
return;
}
// xref 033b
m.playerOK = 1;
// xref 03b0
if (m.enableAlienFire == 0)
{
// xref 03b3
if (--m.alienFireDelay == 0)
m.enableAlienFire = 1;
}
// xref 034a
uint8_t x = m.playerDesc.pos.x;
uint8_t input = 0;
if (m.gameMode == 0)
{
input = m.nextDemoCmd;
}
else
{
input = ReadInputs();
// Map joystick controls into the same domain as the demo commands
if ((input >> 6) & 0x1)
input = 1;
else if ((input >> 5) & 0x1)
input = 2;
else
input = 0;
}
if (input == 0)
{
// Do nothing
}
else if (input == 1)
{
// Move player to the right
if (x != xpix(185))
{
++x;
m.playerDesc.pos.x = x;
}
}
else if (input == 2)
{
// Move player to the left
if (x != xpix(16))
{
--x;
m.playerDesc.pos.x = x;
}
}
else
{
assert(FALSE);
}
DrawPlayer();
}
// Handle the player's death animation. Resets the stack once complete,
// and re-entry is through player_death(0)
static void HandleBlowingUpPlayer(uint8_t anim)
{
// xref 0296
if (--m.expAnimateTimer != 0)
return;
m.playerOK = 0;
m.enableAlienFire = 0;
m.alienFireDelay = 48;
m.expAnimateTimer = 5;
if (--m.expAnimateCnt != 0)
{
// Still animating the explosion
anim = !anim;
m.playerAlive = anim;
m.playerDesc.spr = ptr_to_word((m.PLAYER_SPRITES + (anim+1) * 16));
DrawPlayer();
return;
}
EraseSimpleSprite(m.playerDesc.pos, 16);
RestoreFromROM(u16_to_ptr(PLAYER_ADDR), PLAYER_SIZE);
SoundBits3Off(0);
if (m.invaded)
return;
// Return to splash screens in demo mode
if (m.gameMode == 0)
return;
yield(YIELD_PLAYER_DEATH);
assert(FALSE);
}
// Handle cleanup tasks related to player dying, such as lives and score adjustment,
// switching players and calling GameOver if necessary.
// If invaded is one, the player loses the game, regardless of number of lives left.
// This routine is called after a stack reset, see on_invaded() and HandleBlowingUpPlayer()
static void player_death(int invaded)
{
// xref 02d4
int switch_players = player_death_sub(invaded);
if (!switch_players)
{
if (!*(GetOtherPlayerAliveFlag()) || !m.twoPlayers)
{
RemoveShip();
NewGame(0, 0, INIT | PROMPT | SHIELDS);
}
}
uint8_t pnum = m.playerDataMSB;
if (pnum & 0x1)
RememberShieldsP1();
else
RememberShieldsP2();
uint8_t adx = 0;
Word apos;
uint8_t* aref = GetRefAlienInfo(&adx, &apos);
*aref = apos.y;
*(aref+1) = apos.x;
*(aref-1) = adx;
// about to switch players.
CopyRAMMirror();
uint8_t carry = pnum & 0x1;
uint8_t pmsb = 0x21; // p1
uint8_t cbit = 0;
if (carry)
{
cbit = 0x20; // cocktail bit=1
pmsb = 0x22;
}
m.playerDataMSB = pmsb; // change players
TwoSecDelay();
m.playerHeader.TimerLSB = 0; // clear player object timer
write_port(5, cbit);
m.soundPort5 = (cbit + 1);
ClearPlayField();
RemoveShip();
// jmp to 079b. (newgame + skip)
NewGame(0, 0, INIT);
}
// Player death cleanup subroutine. Game is lost immediately if invaded=1
// Returns true if caller should switch players, false otherwise
// This subroutine will not return if all players have lost the game, instead it
// will call through to GameOver, which will start a new game
static int player_death_sub(int invaded)
{
if (!invaded)
{
DsableGameTasks();
uint8_t* unused;
// still got some ships, keep going
if (GetNumberOfShips(&unused) != 0)
return FALSE;
PrintNumShips(0);
}
// handle losing the game
*CurPlyAlive() = 0;
uint8_t* sc = GetScoreDescriptor();
sc++;
uint8_t* hi = &m.HiScor.h;
uint8_t hi_score_msb = *(hi);
uint8_t pl_score_msb = *(sc);
hi--;
sc--;
uint8_t hi_score_lsb = *(hi);
uint8_t pl_score_lsb = 0;
int higher = FALSE;
if (hi_score_msb == pl_score_msb)
{
// same msb, must check lower
pl_score_lsb = *(sc);
higher = pl_score_lsb > hi_score_lsb;
}
else
{
higher = pl_score_msb > hi_score_msb;
}
if (higher)
{
*hi++ = *sc++;
*hi = *sc;
PrintHiScore();
}
// xref 1698
if (m.twoPlayers)
{
// Game over player<n>
Word sc = xytosc(32, 24);
sc = PrintMessageDel(sc, m.MSG_GAME_OVER__PLAYER___, 0x14);
sc.u16 -= xysc(16, 0); // back up to player indicator
uint8_t b = (m.playerDataMSB & 0x1) ? 0x1b : 0x1c;
sc = DrawChar(sc, b); // print player num
OneSecDelay();
if (*(GetOtherPlayerAliveFlag()) == 0)
{
GameOver(); // won't return.
assert(FALSE);
}
// switch players
return TRUE;
}
else
{
GameOver(); // won't return
assert(FALSE);
}
return FALSE;
}
// Generic player shot drawing routine, originally multiple small fragments
static void DrawPlayerShot(int op)
{
// xref 0404
// xref 03f4
SprDesc plyshot = ReadPlyShot();
if (op == OP_BLEND)
DrawShiftedSprite(&plyshot);
else if (op == OP_ERASE)
EraseShifted(&plyshot);
else
assert(FALSE);
}
// Handles player bullet movement, collision detection and rendering.
// At the end of the routine, the player's shot count is used to
// set up the next saucer direction and score.
static void GameObj1(uint8_t* unused)
{
// xref 03bb
//
// Shot states:
//
// :Available(0), :Initiated(1), :Moving(2), :HitNotAlien(3),
// :AlienExploded(4), :AlienExploding(5)
if (!CompXrToBeam(&m.playerShotDesc.pos.x))
return;
uint8_t status = m.plyrShotStatus;
if (status == 0)
return;
if (status == 1)
{
// xref 03fa InitPlyShot
m.plyrShotStatus = 2;
m.playerShotDesc.pos.x = m.playerDesc.pos.x + 8;
DrawPlayerShot(OP_BLEND);
return;
}
else if (status == 2)
{
// xref 040a MovePlyShot
SprDesc copy = ReadPlyShot();
EraseShifted(©);
copy.pos.y += m.shotDeltaYr;
m.playerShotDesc.pos.y = copy.pos.y;
DrawSprCollision(©);
uint8_t collided = m.collision;
if (!collided) return;
m.alienIsExploding = collided;
return;
}
if (status != 3)
{
// xref 042a
if (status == 5) return;
// continues at EndOfBlowup
}
else
{
// xref 03d7
if (--m.blowUpTimer != 0)
{
// The shot is blowing up
if (m.blowUpTimer != 0x0f) return;
// Draw the explosion the first time through
// xref 03df
DrawPlayerShot(OP_ERASE);
// Change the shot sprite to the explosion.
m.playerShotDesc.spr.l++;
// Modify the coords slightly for the explosion
m.playerShotDesc.pos.y--; // y -= 2
m.playerShotDesc.pos.y--;
m.playerShotDesc.pos.x--; // x -= 3
m.playerShotDesc.pos.x--;
m.playerShotDesc.pos.x--;
m.playerShotDesc.n = 8;
DrawPlayerShot(OP_BLEND);
return;
}
}
// xref 0436 EndOfBlowup
DrawPlayerShot(OP_ERASE);
// xref 0444
// reset the shot
RestoreFromROM(u16_to_ptr(PLAYER_SHOT_DATA_ADDR), PLAYER_SHOT_DATA_SIZE);
// The remaining code in GameObj0 is to do with adjusting the saucer bonus and
// direction, which is changed up on every player shot fired.
// Adjust the saucer bonus.
{
Word table = m.sauScore;
table.l++;
if (table.l >= 0x63)
table.l = 0x54;
m.sauScore = table;
}
Word shots = m.shotCount;
shots.l++;
m.shotCount = shots;
// xref 0461
// If saucer still on screen, don't reset the direction.
if (m.saucerActive)
return;
// xref 0462
//
// This code is using the shot counter as an index into the ROM
// (where some code resides), for the purposes of random number generation.
//
// For the saucer direction logic, only bit 0 of each bytecode is used.
//
// The 256 bytes used reside at 0800 -> 08FF
//
// If you check bit 0 of each byte in that ROM section, you will find that there is no bias,
// and there are exactly 128 0's and 128 1's.
//
// It seems unlikely that this was an accident, I think Nishikado deliberately constructed
// the ROM this way, and used some well placed NOPs to achieve fair balance.
//
// E.g. these NOPs
//
// 0854: 00 00 00
// 0883: 00 00 00
// This information can be exploited to the player's advantage.
//
// If using the shot counting trick to get high scores, the
// expected saucer direction for the first 6 saucers (if counting),
// will be as follows:
//
// [22,37,52,67,82,97]
// [0, 1, 1, 0, 1, 1]
// [L, R, R, L, R, R]
uint8_t v = *(word_to_ptr(shots));
// if lo bit of res is 0, then delta = -2, x = 192 (moving left from rhs)
// if lo bit of res is 1, then delta = +2, x = 9 (moving right from lhs)
uint8_t delta = -2;
uint8_t x = xpix(192);
if (v & 0x1)
{
delta = 2;
x = xpix(9);
}
m.saucerDesc.pos.x = x;
m.saucerDXr = delta;
}
// Return a copy of the player shot sprite descriptor
static SprDesc ReadPlyShot()
{
// xref 0430
return ReadDesc(&m.playerShotDesc);
}
// Handles alien rolling shot firing, movement, collision detection and rendering.
// This is the shot that specifically targets the player.
// Most of the logic is shared between the 3 types inside HandleAlienShot.
static void GameObj2(uint8_t* unused1)
{
// xref 0476
RestoreFromROM(&m.rolShotHeader.TimerExtra, 1);
if (m.rolShotData.CFir.u16 == 0)
{
// The rolling shot doesn't use a firing table to choose a column to fire
// from, because it specifically targets the player.
//
// It just uses this member as a flag to delay firing the first rolling shot
m.rolShotData.CFir.u16 = 0xffff;
return;
}
ToShotStruct(&m.rolShotData, ROL_SHOT_PICEND);
m.otherShot1 = m.pluShotData.StepCnt;
m.otherShot2 = m.squShotData.StepCnt;
HandleAlienShot();
if (m.aShot.BlowCnt != 0)
{
// shot still running, copy updated data from active -> rolling and return.
FromShotStruct(&m.rolShotData);
return;
}
RestoreFromROM(u16_to_ptr(ROLLING_SHOT_ADDR), ROLLING_SHOT_SIZE);
}
// Handles alien plunger shot firing, movement, collision detection and rendering.
static void GameObj3(uint8_t* unused)
{
// xref 04b6
if (m.skipPlunger) return;
if (m.shotSync != 1) return;
ToShotStruct(&m.pluShotData, PLU_SHOT_PICEND);
m.otherShot1 = m.rolShotData.StepCnt;
m.otherShot2 = m.squShotData.StepCnt;
HandleAlienShot();
if (m.aShot.CFir.l >= 16)
{
m.aShot.CFir.l = *(rompos(&m.pluShotData.CFir.l));
}
if (m.aShot.BlowCnt)
{
FromShotStruct(&m.pluShotData);
return;
}
RestoreFromROM(u16_to_ptr(PLUNGER_SHOT_ADDR), PLUNGER_SHOT_SIZE);
if (m.numAliens == 1)
m.skipPlunger = 1;
m.pluShotData.CFir = m.aShot.CFir;
}
// Handles alien squiggly shot firing, movement, collision detection and rendering.
// This is very similar logic to the plunger shot except the column firing table
// is different.
static void ProcessSquigglyShot()
{
// xref 050f
ToShotStruct(&m.squShotData, SQU_SHOT_PICEND);
m.otherShot1 = m.pluShotData.StepCnt;
m.otherShot2 = m.rolShotData.StepCnt;
HandleAlienShot();
if (m.aShot.CFir.l >= 21)
{
// Restores to the rom lsb values of '6'
m.aShot.CFir.l = *(rompos(&m.squShotData.CFir.l));
}
if (m.aShot.BlowCnt)
{
FromShotStruct(&m.squShotData);
return;
}
RestoreFromROM(u16_to_ptr(SQUIGGLY_SHOT_ADDR), SQUIGGLY_SHOT_SIZE);
m.squShotData.CFir = m.aShot.CFir;
}
// Copy an alien shot structure from src into the active alien shot structure,
// and configure the shot animation.
static void ToShotStruct(AShot* src, uint8_t picend)
{
// xref 0550
m.shotPicEnd = picend;
BlockCopy(&m.aShot, src, 11);
}
// Copy the active alien shot structure into dest
static void FromShotStruct(AShot* dest)
{
// xref 055b
BlockCopy( dest, &m.aShot, 11);
}
// This logic is shared between the 3 shot types.
// Handles shot firing, movement, collision detection and rendering.
static void HandleAlienShot()
{
// xref 0563
if ((m.aShot.Status & SHOT_ACTIVE) != 0)
{
HandleAlienShotMove();
return;
}
uint8_t shooting_c = (m.isrSplashTask == 0x04);
uint8_t fire_enabled = m.enableAlienFire;
if (shooting_c)
{
// Special case for the splash animation
m.aShot.Status |= SHOT_ACTIVE;
m.aShot.StepCnt++;
return;
}
if (!fire_enabled)
return;
m.aShot.StepCnt = 0;
{
uint8_t steps = m.otherShot1;
if (steps && steps <= m.aShotReloadRate) return;
}
{
uint8_t steps = m.otherShot2;
if (steps && steps <= m.aShotReloadRate) return;
}
uint8_t col = 0;
if (m.aShot.Track == 0)
{
// xref 061b
// Make a tracking shot, by finding the column that is above the player
Word res = FindColumn(m.playerDesc.pos.x + 8); // find column over centre of player
col = res.h; // res.l unused
if (col >= 12)
col = 11;
}
else
{
// xref 059c
// Use the firing table pointer to pick the column, and advance it
uint8_t* hl = word_to_ptr(m.aShot.CFir);
col = *hl++;
m.aShot.CFir = ptr_to_word(hl);
}
// xref 05a5
uint8_t k = 0;
uint8_t found = FindInColumn(&k, col);
if (!found)
return;
uint8_t row_unused = 0;
Word pixnum = GetAlienCoords(&row_unused, k);
pixnum.x += 7;
pixnum.y -= 10;
m.aShot.Desc.pos = pixnum;
m.aShot.Status |= SHOT_ACTIVE;
m.aShot.StepCnt++;
return;
}
// Handle moving the alien shot and some collision detection response.
// Returns 1 if shot status needs to be set to blowing up, 0 if not.
static int DoHandleAlienShotMove()
{
if (!CompXrToBeam(&m.aShot.Desc.pos.x))
return 0;
if (m.aShot.Status & SHOT_BLOWUP)
{
ShotBlowingUp();
return 0;
}
// xref 05cf
m.aShot.StepCnt++;
EraseAlienShot();
// Animate the shot
uint8_t shotpic = m.aShot.Desc.spr.l + 3;
if (shotpic > m.shotPicEnd)
shotpic -= 12;
m.aShot.Desc.spr.l = shotpic;
m.aShot.Desc.pos.y = m.aShot.Desc.pos.y + m.alienShotDelta;
DrawAlienShot();
// xref 05f3
uint8_t y = m.aShot.Desc.pos.y;
if (y < 21)
return 1;
if (!m.collision)
return 0;
y = m.aShot.Desc.pos.y;
// below or above players area ?
if (y < 30 || y >= 39)
return 1;
if (!is_godmode())
m.playerAlive = 0;
return 1;
}
// Handle moving the alien shot and some collision detection response.
static void HandleAlienShotMove()
{
// xref 05c1
int exploded = DoHandleAlienShotMove();
if (exploded)
m.aShot.Status |= SHOT_BLOWUP;
}
// Find a live alien in the given column.
static uint8_t FindInColumn(uint8_t *out, uint8_t col)
{
// xref 062f
Word hl;
hl.h = m.playerDataMSB;
hl.l = col - 1;
int found = 0;
for (int i = 0; i < 5; ++i)
{
if (*word_to_ptr(hl))
{
found = 1;
break;
}
hl.l += 11;
}
*out = hl.l;
return found;
}
// Handle alien shot explosion animation
static void ShotBlowingUp()
{
// xref 0644
m.aShot.BlowCnt--;
uint8_t blowcnt = m.aShot.BlowCnt;
if (blowcnt == 3)
{
EraseAlienShot();
m.aShot.Desc.spr = ptr_to_word(m.AlienShotExplodingSprite);
// Offset the explision sprite from the shot by (-2,-2)
m.aShot.Desc.pos.x--;
m.aShot.Desc.pos.x--;
m.aShot.Desc.pos.y--;
m.aShot.Desc.pos.y--;
m.aShot.Desc.n = 6;
DrawAlienShot();
return;
}
if (blowcnt)
return;
EraseAlienShot();
}
// Draw the active alien shot and do collision detection
static void DrawAlienShot()
{
// xref 066c
SprDesc desc = ReadDesc(&m.aShot.Desc);
DrawSprCollision(&desc);
return;
}
// Erase the active alien shot
static void EraseAlienShot()
{
// xref 0675
SprDesc desc = ReadDesc(&m.aShot.Desc);
EraseShifted(&desc);
return;
}
// Handles either the Squiggly shot or the Saucer, depending on the saucer timer.
// See ProcessSquigglyShot for squiggly shot logic.
// The bulk of this routine handles saucer movement, collision response, rendering
// and scoring.
static void GameObj4(uint8_t* unused)
{
// xref 0682
if (m.shotSync != 2) return;
if (m.saucerStart == 0)
{
ProcessSquigglyShot();
return;
}
if (m.squShotData.StepCnt)
{
ProcessSquigglyShot();
return;
}
if (!m.saucerActive)
{
if (m.numAliens < 8)
{
ProcessSquigglyShot();
return;
}
m.saucerActive = 1;
DrawSaucer();
}
uint8_t carry = CompXrToBeam(&m.saucerDesc.pos.x);
if (!carry)
return;
if (!m.saucerHit)
{
uint8_t x = m.saucerDesc.pos.x;
uint8_t dx = m.saucerDXr;
m.saucerDesc.pos.x = x + dx;
DrawSaucer();
x = m.saucerDesc.pos.x;
// check edges
if (x < xpix(8))
{
RemoveSaucer();
return;
}
if (x >= xpix(193))
{
RemoveSaucer();
return;
}
return;
}
SoundBits3Off(0xfe); // turn off saucer sound
m.saucerHitTime--;
uint8_t timer = m.saucerHitTime;
if (timer == 31)
{
// xref 074b
// Turn on the sound and draw the saucer explosion
uint8_t snd = m.soundPort5 | 16;
m.soundPort5 = snd;
SetSoundWithoutFleet(snd);
m.saucerDesc.spr = ptr_to_word(m.SpriteSaucerExp);
DrawSaucer();
return;
}
if (timer == 24)
{
// xref 070c
m.adjustScore = 1;
// Get the score for the saucer which is set based on shots fired in GameObj0
uint8_t score = *(word_to_ptr(m.sauScore));
// Find the index of the score in the table
int i = 0;
for (i = 0; i < 4; ++i)
{
if (m.SauScrValueTab[i] == score)
break;
}
// Use it to find the matching LSB for the score text, and set it in saucerDesc
m.saucerDesc.spr.l = m.SauScrStrTab[i];
// Multiply the score by 16 (i.e. bcd shift left one digit), to get 50,100,150,300 in BCD
m.scoreDelta.u16 = score * 16;
// Print the bonus score message, using pointer set in saucerDesc.spr above
SprDesc desc = GetSaucerInfo();
PrintMessage(desc.sc, word_to_ptr(desc.spr), 3);
return;
}
// xref 06e8
if (timer != 0)
return;
uint8_t snd = m.soundPort5 & 0xef;
m.soundPort5 = snd;
write_port(5, snd & 0x20);
RemoveSaucer();
}
// Saucer cleanup tasks
static void RemoveSaucer()
{
// xref 06f9
SprDesc desc = ReadDesc(&m.saucerDesc);
ClearSmallSprite(ConvToScr(desc.pos), desc.n, 0);
RestoreFromROM(u16_to_ptr(SAUCER_ADDR), SAUCER_SIZE);
SoundBits3Off(0xfe);
}
// Grab a copy of the saucer sprite descriptor, and set it up
// for rendering before returning it.
static SprDesc GetSaucerInfo()
{
// xref 0742
SprDesc desc = ReadDesc(&m.saucerDesc);
desc.sc = ConvToScr(desc.pos);
return desc;
}
// Draw the player sprite
static void DrawPlayer()
{
// xref 036f
SprDesc desc = ReadDesc(&m.playerDesc);
desc.sc = ConvToScr(desc.pos);
DrawSimpSprite(&desc);
m.playerHeader.TimerExtra = 0;
}
// Draw the saucer sprite
static void DrawSaucer()
{
// xref 073c
// xref 0742
SprDesc desc = GetSaucerInfo();
DrawSimpSprite(&desc);
}
// Wait for the player to press 1P or 2P, and then start the game
// with the appropriate flags.
// This loop is entered after the player has inserted a coin outside of game mode, see vblank_int()
static void WaitForStart()
{
// xref 076e
{
// xref 1979
// SuspendGameTasks
DsableGameTasks();
DrawNumCredits();
PrintCreditLabel();
}
ClearPlayField();
PrintMessage(xytosc(96, 152), m.MSG_PUSH, 4);
while (TRUE)
{
timeslice();
if ((m.numCoins - 1) != 0)
{
// Enough credits for either 1P or 2P start
PrintMessage(xytosc(32, 128), m.MSG_1_OR_2PLAYERS_BUTTON, 20);
// Handle 1P or 2P
uint8_t inp = read_port(1);
if (inp & 0x2) NewGame(1, 0x98, 0);
if (inp & 0x4) NewGame(0, 0x99, 0);
continue;
}
// Only enough credits for 1P start
PrintMessage(xytosc(32, 128), m.MSG_ONLY_1PLAYER__BUTTON, 20);
// Break if 1P start hit.
if (read_port(1) & 0x4)
break;
}
NewGame(0, 0x99, 0);
}
// Starts a new game, and runs the game loop.
// This routine is entered via either the WaitForStart() loop after inserting coins
// outside of game mode, or is entered after the player dies via player_death()
// to continue the game.
// is2p - set to true if 2P was pressed
// cost - credits to deduct in bcd (0x99=1, 0x98=2 credits)
// skip - used to skip certain parts of initialization, used for continue
static void NewGame(uint8_t is2p, uint8_t cost, int skip)
{
// xref 0798
// xref 079b
int flags = ~skip;
if (flags & INIT)
{
m.twoPlayers = is2p;
{
uint8_t unused_carry = 0;
m.numCoins = bcd_add(m.numCoins, cost, &unused_carry);
}
DrawNumCredits();
m.P1Scor.u16 = 0;
m.P2Scor.u16 = 0;
PrintP1Score();
PrintP2Score();
DsableGameTasks();
m.gameMode = 1;
m.playerStates.u16 = 0x0101; // Both players alive
m.playerExtras.u16 = 0x0101; // Both players bonus available
DrawStatus();
DrawShieldP1();
DrawShieldP2();
uint8_t ships = GetShipsPerCred();
m.p1ShipsRem = ships;
m.p2ShipsRem = ships;
InitAlienRacks();
m.p1RackCnt = 0;
m.p2RackCnt = 0;
InitAliensP1();
InitAliensP2();
m.p1RefAlienPos = xytopix(24, 120);
m.p2RefAlienPos = xytopix(24, 120);
CopyRAMMirror();
RemoveShip();
}
if (flags & PROMPT)
{
PromptPlayer();
ClearPlayField();
m.isrSplashTask = 0;
}
// xref 0804 top of new game loop
while (TRUE)
{
if (flags & SHIELDS)
{
DrawBottomLine();
if (m.playerDataMSB & 0x1)
{
RestoreShieldsP1();
}
else
{
RestoreShieldsP2();
DrawBottomLine();
}
// xref 0814
InitRack();
}
else
{
flags |= SHIELDS; // don't skip next time
}
EnableGameTasks();
SoundBits3On(0x20);
// xref 081f game loop
while (TRUE)
{
PlrFireOrDemo();
PlyrShotAndBump();
CountAliens();
AdjustScore();
if (m.numAliens == 0)
{
HandleEndOfTurn();
break;
}
AShotReloadRate();
CheckAndHandleExtraShipAward();
SpeedShots();
ShotSound();
if (! IsPlayerAlive())
SoundBits3On(0x04); // Turn on player hit sound
uint8_t w = FleetDelayExShip();
write_port(6, w); // Feed the watchdog
CtrlSaucerSound();
}
}
}
// Get reference alien velocity, position and pointer for the current player
static uint8_t* GetRefAlienInfo(uint8_t *dxr, Word *pos)
{
// xref 0878
*dxr = m.refAlienDelta.x;
*pos = m.refAlienPos;
return GetAlRefPtr();
}
// Get reference alien pointer for the current player
static uint8_t* GetAlRefPtr()
{
// xref 0886
return (m.playerDataMSB & 0x1) ? &m.p1RefAlienPos.l :
&m.p2RefAlienPos.l;
}
// Print "PLAY PLAYER<n>" and flash the score at 15 hz for 3 seconds
// This is done upon starting a NewGame in 1P mode, or at the start
// of every turn in 2P mode.
static void PromptPlayer()
{
// xref 088d
// "PLAY PLAYER<1>"
PrintMessage(xytosc(56,136), m.MSG_PLAY_PLAYER_1_, 14);
// replace <1> with <2>
if ((m.playerDataMSB & 0x1) == 0)
DrawChar(xytosc(152, 136), 0x1c);
m.isrDelay = 176; // 3 sec delay
// xref 08a9
while (TRUE)
{
timeslice();
uint8_t isrtick = m.isrDelay;
if (isrtick == 0)
return;
// Flash player score every 4 isrs
if (isrtick & 0x4)
{
// xref 08bc
Word sc = (m.playerDataMSB & 0x1) ? xytosc(24,224) : xytosc(168,224);
ClearSmallSprite(sc, 32, 0);
continue;
}
DrawScore(GetScoreDescriptor());
}
}
// DIP5 and DIP3 control the number of extra lives the player starts with.
// DIP5 and DIP3 are wired into bits 1 and 0 of port 2 respectively.
//
// When read together as a two digit binary number, this is meant to be
// interpreted as the number of extra lives above the default of 3 that
// the player gets.
//
// 0 0 - 3 lives
// 0 1 - 4 lives
// 1 0 - 5 lives
// 1 1 - 6 lives
static uint8_t GetShipsPerCred()
{
// xref 08d1
return (read_port(2) & (DIP5_SHIPS2 | DIP3_SHIPS1)) + 3;
}
// Increase alien shot speed when there are less than nine aliens on screen
static void SpeedShots()
{
// xref 08d8
if (m.numAliens >= 9)
return;
m.alienShotDelta = -5; // from -4 to -5
}
// Prints a text message (msg, n) on the screen at pos
// Used to print all the splash screen text, and other game messages
static void PrintMessage(Word sc, uint8_t* msg, size_t n)
{
// xref 08f3
// DebugMessage(sc, msg, n);
for (size_t i = 0; i < n; ++i)
{
uint8_t c = msg[i];
sc = DrawChar(sc, c);
}
}
// Draw a text character c on the screen at pos
// Used by PrintMessage()
static Word DrawChar(Word sc, uint8_t c)
{
// xref 08ff
SprDesc desc;
desc.sc = sc;
desc.spr = u16_to_word(0x1e00 + c*8);
desc.n = 8;
return DrawSimpSprite(&desc);
}
// Timing logic that controls when the saucer appears (Every 25.6 secs)
// Called via vblank_int()
static void TimeToSaucer()
{
// xref 0913
// No ticking until alien rack has dropped down a bit
if (m.refAlienPos.y >= 120)
return;
uint16_t timer = m.saucerTimer.u16;
if (timer == 0)
{
timer = 0x600; // reset timer to 1536 game loops (25.6s)
m.saucerStart = 1;
}
m.saucerTimer.u16 = timer - 1;
}
// Get number of lives for the current player
static uint8_t GetNumberOfShips(uint8_t* *ptr)
{
// xref 092e
*ptr = (GetPlayerDataPtr() + 0xff);
return *(*ptr);
}
// Award the one and only bonus life if the player's score is high enough,
// and fix up the lives indicators to reflect that.
static void CheckAndHandleExtraShipAward()
{
// xref 0935
if (*(CurPlyAlive() - 2) == 0)
return;
// Bonus dip bit - award at 1000 or 1500
uint8_t b = (read_port(2) & DIP6_BONUS) ? 0x10 : 0x15;
uint8_t score_msb = *(GetScoreDescriptor() + 1);
// score not high enough for bonus yet
if (score_msb < b)
return;
uint8_t* nships_ptr;
GetNumberOfShips(&nships_ptr);
// Award the bonus life
(*nships_ptr)++;
int nships = *nships_ptr;
SprDesc desc;
desc.sc = xytosc(8 + 16 * nships, 8);
desc.spr = ptr_to_word(m.PLAYER_SPRITES);
desc.n = 16;
DrawSimpSprite(&desc);
PrintNumShips(nships+1);
*(CurPlyAlive() - 2) = 0; // Flag extra ship has been awarded
m.extraHold = 0xff; // Handle Extra-ship sound
SoundBits3On(0x10);
}
// Lookup score for alien based on the given row
static uint8_t* AlienScoreValue(uint8_t row)
{
// xref 097c
uint8_t si = 0;
if (row < 2) si = 0;
else if (row < 4) si = 1;
else si = 2;
return (m.AlienScores + si);
}
// Add the score delta to the current player's score, and draw it.
// Called as part of the game loop.
//
// scoreDelta is modified in two places:
// PlayerShotHit() upon killing an alien (main thread)
// GameObj4() upon hitting the saucer (either vblank or mid depending on saucer x pos)
static void AdjustScore()
{
// xref 0988
uint8_t* sptr = GetScoreDescriptor();
if (m.adjustScore == 0) return;
m.adjustScore = 0;
Word adj = m.scoreDelta;
uint8_t carry = 0;
Word score;
score.l = *(sptr);
score.l = bcd_add(score.l, adj.l, &carry);
*sptr = score.l;
score.h = *(sptr+1);
score.h = bcd_add(score.h, adj.h, &carry);
*(sptr+1) = score.h;
Word sc;
sc.l = *(sptr+2);
sc.h = *(sptr+3);
Print4Digits(sc, score);
}
// Print 4 digits using the bcd values in val.h and val.l
// Called via DrawScore and AdjustScore
static void Print4Digits(Word sc, Word val)
{
// xref 09ad
sc = DrawHexByte(sc, val.h);
sc = DrawHexByte(sc, val.l);
}
// Draw the the hi and lo nibble of the bcd value in c at sc
static Word DrawHexByte(Word sc, uint8_t c)
{
// xref 09b2
sc = DrawHexByteSub(sc, c >> 4);
sc = DrawHexByteSub(sc, c & 0xf);
return sc;
}
// Draw the digit in c at sc
static Word DrawHexByteSub(Word sc, uint8_t c)
{
// xref 09c5
return DrawChar(sc, c + 0x1a);
}
// Return a pointer to the score info for the current player
static uint8_t* GetScoreDescriptor()
{
// xref 09ca
return (m.playerDataMSB & 0x1) ? &m.P1Scor.l : &m.P2Scor.l;
}
// Clear the play field in the center of the screen.
// Horizontally, the play field is the full width of the screen.
// Vertically, the play field is the area above the lives and credits (16 pixels)
// and below the scores (32 pixels).
static void ClearPlayField()
{
// xref 09d6
uint8_t* screen = m.vram;
for (int x = 0; x < 224; ++x)
{
screen += 2;
for (int b = 0; b < 26; ++b)
*screen++ = 0;
screen += 4;
}
}
// Called from the game loop when the player has killed all aliens in the rack.
static void HandleEndOfTurn()
{
// xref 09ef
HandleEndOfTurnSub(); // wait for player to finish dying if necessary
m.gameTasksRunning = 0;
ClearPlayField();
uint8_t pnum = m.playerDataMSB;
CopyRAMMirror();
m.playerDataMSB = pnum;
pnum = m.playerDataMSB; // redundant load
uint8_t rack_cnt = 0;
uint8_t* rcptr = u16_to_ptr(pnum << 8 | 0xfe);
rack_cnt = (*rcptr % 8) + 1;
*rcptr = rack_cnt;
// Starting Y coord for rack for new level
uint8_t y = m.AlienStartTable[rack_cnt-1];
uint8_t* refy = u16_to_ptr(pnum << 8 | 0xfc);
uint8_t* refx = refy + 1;
*refy = y;
*refx = 56;
if (!(pnum & 0x1))
{
m.soundPort5 = 0x21; // start fleet with first sound
DrawShieldP2();
InitAliensP2();
}
else
{
DrawShieldP1();
InitAliensP1();
}
}
// Called at start of HandleEndOfTurnSub() to handle the
// case of the player dying at the end of turn.
// (i.e. last alien and player both kill each other)
static void HandleEndOfTurnSub()
{
// xref 0a3c
if (IsPlayerAlive())
{
m.isrDelay = 48; // wait up to 3/4 of a sec
do
{
// xref 0a47
timeslice(); // spin
if (m.isrDelay == 0)
return;
} while (IsPlayerAlive());
}
// If player is not alive, wait for resurrection
while (!IsPlayerAlive())
{
// xref 0a52
timeslice(); // spin
}
}
// Returns 1 if player is alive, 0 otherwise
static uint8_t IsPlayerAlive()
{
// xref 0a59
return m.playerAlive == 0xff;
}
// Called as part of the player bullet collision response in PlayerShotHit()
// when the player bullet kills an alien.
static void ScoreForAlien(uint8_t row)
{
// xref 0a5f
if (!m.gameMode)
return;
SoundBits3On(0x08);
uint8_t score = *(AlienScoreValue(row));
m.scoreDelta.h = 0;
m.scoreDelta.l = score;
m.adjustScore = 1;
}
// Companion routine to SplashSprite
// Called from the main thread to initiate and wait for splash animations (CCOIN / PLAy).
static void Animate()
{
// xref 0a80
// Directs ISRSplTasks() (in vblank_int()) to call SplashSprite()
m.isrSplashTask = 2;
// Spin until sprite in animation reaches its target position
do {
// xref 0a85
write_port(6, 2); // feed watchdog and spin
}
while (!m.splashReached);
// Directs ISRSplTasks() to do nothing
m.isrSplashTask = 0;
}
// Prints the animated text messages (such as "PLAY" "SPACE INVADERS"),
// by drawing the characters that make up the message with a short
// delay between them. (7 frames per character)
static Word PrintMessageDel(Word sc, uint8_t* str, uint8_t n)
{
// xref 0a93
// DebugMessage(sc, str, n);
for (int i = 0; i < n; ++i)
{
sc = DrawChar(sc, str[i]);
m.isrDelay = 7;
while (m.isrDelay != 1) { timeslice(); } // spin
}
return sc;
}
// Need for the shooting C in CCOIN animation.
// Initiated in AnimateShootingSplashAlien(), and called
// via ISRSplTasks() during vblank_int()
static void SplashSquiggly()
{
// xref 0aab
// this works because this is the last game object.
RunGameObjs(u16_to_ptr(SQUIGGLY_SHOT_ADDR));
}
// Wait for approximately one second. (64 vblanks).
static void OneSecDelay()
{
// xref 0ab1
WaitOnDelay(64);
}
// Wait for approximately two seconds. (128 vblanks).
static void TwoSecDelay()
{
// xref 0ab6
WaitOnDelay(128);
}
// Runs the game objects in demo mode to attract players.
// Initiated from the main thread and called
// via ISRSplTasks() during vblank_int()
static void SplashDemo()
{
// xref 0abb
// xref 0072
m.shotSync = m.rolShotHeader.TimerExtra;
DrawAlien();
RunGameObjs(u16_to_ptr(PLAYER_ADDR)); // incl player
TimeToSaucer();
}
// Runs the appropriate splash screen task (from vblank_int())
static void ISRSplTasks()
{
// xref 0abf
switch (m.isrSplashTask)
{
case 1: SplashDemo(); break; // Attract players with game demo
case 2: SplashSprite(); break; // Moves a sprite to a target location for an animation
case 4: SplashSquiggly(); break; // Run an alien shot for an animation ( CCOIN )
}
}
// Print an animated message in the center of the screen.
static void MessageToCenterOfScreen(uint8_t* str)
{
// xref 0acf
PrintMessageDel(xytosc(56,160), str, 0x0f);
}
// Wait for n vblank interrupts to occur, using m.isrDelay
static void WaitOnDelay(uint8_t n)
{
// xref 0ad7
// Wait on ISR counter to reach 0
m.isrDelay = n;
while (m.isrDelay != 0) { timeslice(); } // spin
}
// Copy src into the splash animation structure.
// The four animations copied this way are
//
// (for PLAy animation)
//
// 0x1a95 - Move alien left to grab y
// 0x1bb0 - Move alien (with y) to right edge
// 0x1fc9 - Bring alien back (with Y) to message
//
// (for CCOIN animation)
//
// 0x1fd5 - Move alien to point above extra 'C'
static void IniSplashAni(uint8_t* src)
{
// xref 0ae2
BlockCopy(&m.splashAnForm, src, 12);
}
// Called during (splash screens) to do some miscellaneous tasks
// a) Player shot collision response
// b) Detecting and handling the alien rack bumping the screen edges
// c) Checking for TAITO COP input sequence
static uint8_t CheckPlyrShotAndBump()
{
// xref 0bf1
PlyrShotAndBump();
CheckHiddenMes();
return 0xff;
}
// Erases a sprite by clearing the four bytes it
// could possibly be in.
static void EraseSimpleSprite(Word pos, uint8_t n)
{
// xref 1424
Word sc = CnvtPixNumber(pos);
for (int i = 0; i < n; ++i, sc.u16 += xysc(1, 0))
{
uint8_t* screen = word_to_ptr(sc);
*screen++ = 0;
*screen++ = 0;
}
}
// Draws a non shifted sprite from desc->spr horizontally
// across the screen at desc->pos for desc->n bytes.
// Each byte of the sprite is a vertical 8 pixel strip
static Word DrawSimpSprite(SprDesc *desc)
{
// xref 1439
uint8_t* screen = word_to_ptr(desc->sc);
uint8_t* sprite = word_to_ptr(desc->spr);
for (size_t i = 0; i < desc->n; ++i, screen += xysc(1, 0))
*screen = sprite[i];
return ptr_to_word(screen);
}
// Using pixnum, set the shift count on the hardware shifter
// and return the screen coordinates for rendering
static Word CnvtPixNumber(Word pos)
{
// xref 1474
write_port(2, (pos.u16 & 0xff) & 0x07);
return ConvToScr(pos);
}
// Draw a shifted sprite to the screen, blending with screen contents
static void DrawShiftedSprite(struct SprDesc *desc)
{
// xref 1400
DrawSpriteGeneric(desc, OP_BLEND);
}
// Erase a shifted sprite from the screen, zeroing screen contents
static void EraseShifted(struct SprDesc *desc)
{
// xref 1452
DrawSpriteGeneric(desc, OP_ERASE);
}
// Draw a shifted sprite to the screen, blending with screen contents,
// and detect if drawn sprite collided with existing pixels
static void DrawSprCollision(struct SprDesc *desc)
{
// xref 1491
DrawSpriteGeneric(desc, OP_COLLIDE);
}
// Draw a shifted sprite to the screen, overwriting screen contents.
static void DrawSprite(struct SprDesc *desc)
{
// xref 15d3
DrawSpriteGeneric(desc, OP_BLIT);
}
// Generic sprite drawing routine for shifted spries
// desc->spr - source pointer
// desc->pixnum - pixel position to draw at
// desc->n - width
// op - erase | blit | blend | collide
static void DrawSpriteGeneric(struct SprDesc *desc, int op)
{
Word sc = CnvtPixNumber(desc->pos);
uint8_t* sprite = word_to_ptr(desc->spr);
if (op == OP_COLLIDE)
m.collision = 0;
for (int i = 0; i < desc->n; ++i, sc.u16 += xysc(1,0))
{
uint8_t* screen = word_to_ptr(sc);
uint8_t shift_in[2];
shift_in[0] = sprite[i];
shift_in[1] = 0;
for (int j = 0; j < 2; ++j)
{
write_port(4, shift_in[j]); // write into shift reg
uint8_t shifted = read_port(3); // get the shifted pixels (shift based on pix num)
if (op == OP_COLLIDE && (shifted & *screen))
m.collision = 1;
if (op == OP_COLLIDE || op == OP_BLEND)
*screen = shifted | *screen;
else if (op == OP_BLIT)
*screen = shifted;
else if (op == OP_ERASE)
*screen = (shifted ^ 0xff) & *screen;
screen++;
}
}
}
// Repeat (width n) the pixel strip in byte v horizontally across the screen
static Word ClearSmallSprite(Word sc, uint8_t n, uint8_t v)
{
// xref 14cb
for (int i = 0; i < n; ++i, sc.u16 += xysc(1,0))
*(word_to_ptr(sc)) = v;
return sc;
}
// Player bullet collision response
static void PlayerShotHit()
{
// xref 14d8
uint8_t status = m.plyrShotStatus;
// if alien explosion state, bail
if (status == 5)
return;
// if not normal movement, bail
if (status != 2)
return;
// Get the Y coord
uint8_t shoty = m.playerShotDesc.pos.y;
if (shoty >= 216)
{
// missed and hit top of screen
m.plyrShotStatus = 3;
m.alienIsExploding = 0;
SoundBits3Off(0xf7);
return;
}
// xref 14ea
if (!m.alienIsExploding)
return;
if (shoty >= 206)
{
// hit the saucer
// xref 1579
m.saucerHit = 1;
m.plyrShotStatus = 4;
// xref 154a
m.alienIsExploding = 0;
SoundBits3Off(0xf7);
return;
}
shoty += 6;
{
uint8_t refy = m.refAlienPos.y;
// refy can wrap around, if the topmost alien row gets near the bottom of the screen
// in usual play, refy will be < 144.
if ((refy < 144) && (refy >= shoty))
{
// hit the shields
m.plyrShotStatus = 3;
m.alienIsExploding = 0;
SoundBits3Off(0xf7);
return;
}
}
// xref 1504
// Get here if player shot hits an alien or an alien shot.
// There is a subtle bug here, see CodeBug1 in CA
Word res = FindRow(shoty);
uint8_t row = res.h;
uint8_t ay = res.l;
res = FindColumn(m.playerShotDesc.pos.x);
uint8_t col = res.h;
uint8_t ax = res.l;
m.expAlien.pos = u8_u8_to_word(ax, ay);
m.plyrShotStatus = 5;
uint8_t* alienptr = GetAlienStatPtr(row, col);
if (*alienptr == 0)
{
// If alien is dead, then the player shot must have hit an alien shot
m.plyrShotStatus = 3;
m.alienIsExploding = 0;
SoundBits3Off(0xf7);
return;
}
// Kill the alien
*alienptr = 0;
ScoreForAlien(row);
SprDesc desc = ReadDesc(&m.expAlien);
DrawSprite(&desc);
m.expAlienTimer = 16;
}
// Counts the number of 16s between *v and target.
// This is roughly (tgt - *v) / 16.
static uint8_t Cnt16s(uint8_t *v, uint8_t tgt)
{
// xref 1554
uint8_t n = 0;
if ((*v) >= tgt)
{
do
{
// wrap ref
n++;
(*v) += 16;
} while ((*v) & 0x80);
}
while ((*v) < tgt)
{
(*v) += 16;
n++;
}
return n;
}
// Find alien row given y pos
static Word FindRow(uint8_t y)
{
// xref 1562
uint8_t ry = m.refAlienPos.y;
uint8_t rnum = Cnt16s(&ry, y) - 1;
uint8_t coord = (ry - 16);
return u8_u8_to_word(rnum, coord);
}
// Find alien column given x pos
static Word FindColumn(uint8_t x)
{
// xref 156f
uint8_t rx = m.refAlienPos.x;
uint8_t cnum = Cnt16s(&rx, x);
uint8_t coord = (rx - 16);
return u8_u8_to_word(cnum, coord);
}
// Return a pointer to the alien status for the current player
// given the row and column of the alien
static uint8_t* GetAlienStatPtr(uint8_t row, uint8_t col)
{
// xref 1581
// row is 0 based
// col is 1 based
uint8_t idx = row * 11 + (col - 1);
return u16_to_ptr(m.playerDataMSB << 8 | idx);
}
// Change alien deltaX and deltaY when alien rack bumps edges
static void RackBump()
{
// xref 1597
// Change alien deltaX and deltaY when rack bumps edges
uint8_t dx = 0;
uint8_t dir = 0;
if (m.rackDirection == 0)
{
// xref 159e check right edge
if (!RackBumpEdge(xytosc(213,32)))
return;
dx = -2;
dir = 1; // rack now moving left
}
else
{
// check left edge
if (!RackBumpEdge(xytosc(9,32)))
return;
// rack now moving right
// inline 18f1
dx = m.numAliens == 1 ? 3 : 2; // go faster if only one alien remaining
dir = 0; // rack now moving right
}
m.rackDirection = dir;
m.refAlienDelta.x = dx;
m.refAlienDelta.y = m.rackDownDelta;
}
// Check 23 bytes vertically up from sc for pixels.
// Used by RackBump to detect whether alien rack is hitting the edges of the play area.
static uint8_t RackBumpEdge(Word sc)
{
// xref 15c5
uint8_t* screen = word_to_ptr(sc);
for (int i = 0; i < 23; ++i)
{
timeslice();
// found some pixels
if (*screen++)
return 1;
}
return 0;
}
// Count the number of live aliens for the current player
static void CountAliens()
{
// xref 15f3
uint8_t* iter = GetPlayerDataPtr(); // Get active player descriptor
uint8_t n = 0;
for (int i = 0; i < 55; ++i)
{
timeslice();
if (*iter++ != 0)
++n;
}
m.numAliens = n;
if (n != 1)
return;
// Apparently unused
*(u16_to_ptr(0x206b)) = 1;
}
// Return a pointer the the current player's RAM area
static uint8_t* GetPlayerDataPtr()
{
// xref 1611
return u16_to_ptr(m.playerDataMSB << 8);
}
// Handles player firing in game and demo mode.
// In both cases, nothing happens if there is a shot on the screen.
// In game mode, reads the fire button and debounces to detect press.
// In demo mode, initiates a shot always, and consumes the next movement command from the buffer.
static void PlrFireOrDemo()
{
// xref 1618
if (m.playerAlive != 0xff)
return;
uint8_t timer_hi = m.playerHeader.TimerMSB;
uint8_t timer_lo = m.playerHeader.TimerLSB;
// Return if not ready yet.
if (timer_lo | timer_hi)
return;
// Return if player has a shot still on screen
if (m.plyrShotStatus)
return;
// xref 162b
if (m.gameMode)
{
// Handle fire button reading and debouncing
uint8_t prev = m.fireBounce;
uint8_t cur = (ReadInputs() & 0x10) != 0;
if (prev == cur)
return;
if (cur)
m.plyrShotStatus = 1; // Flag shot active
m.fireBounce = cur;
return;
}
else
{
// Demo player constantly fires
m.plyrShotStatus = 1;
// Consume demo command from circular buffer 0x1f74 <-> 0x1f7e
//
// DemoCommands:
//; (1=Right, 2=Left)
// 74 75 76 77 78 79 7A 7B 7C 7D 7E
//1F74: 01 01 00 00 01 00 02 01 00 02 01
Word iter = u16_to_word(m.demoCmdPtr.u16 + 1);
if (iter.l >= 0x7e)
iter.l = 0x74; // wrap
m.demoCmdPtr = iter;
m.nextDemoCmd = *(word_to_ptr(iter));
}
}
// Called when all players are dead in game mode
// Prints "GAME OVER" and sets things up to reenter splash screens.
static void GameOver()
{
// xref 16c9
PrintMessageDel(xytosc(72,192), m.MSG_GAME_OVER__PLAYER___, 10);
TwoSecDelay();
ClearPlayField();
m.gameMode = 0;
write_port(5, 0); // all sound off
EnableGameTasks();
main_init(1);
assert(FALSE); // won't return
}
// Called when the player loses the game upon an alien reaching the bottom
static void on_invaded()
{
// xref 16ea
m.playerAlive = 0;
do {
PlayerShotHit();
SoundBits3On(4);
}
while (!IsPlayerAlive());
DsableGameTasks();
DrawNumShipsSub(xytosc(24, 8)); // 19fa
PrintNumShips(0);
// xref 196b
SoundBits3Off(0xfb);
player_death(1);
// won't return
assert(FALSE);
}
// Increases the difficulty of the game as the player's score gets higher by
// decreasing time between alien shots.
static void AShotReloadRate()
{
// xref 170e
uint8_t score_hi = *(GetScoreDescriptor() + 1);
// Uses these tables, in decimal
// 02 16 32 48 (AReloadScoreTab)
// 48 16 11 08 07 (ShotReloadRate)
int i = 0;
// xref 171c
for (i = 0; i < 4; ++i)
{
if (m.AReloadScoreTab[i] >= score_hi)
break;
}
// xref 1727
m.aShotReloadRate = m.ShotReloadRate[i];
}
// Turn player shot sound on/off depending on m.plyrShotStatus
static void ShotSound()
{
// xref 172c
if (m.plyrShotStatus == 0)
{
SoundBits3Off(0xfd);
return;
}
SoundBits3On(0x02);
}
// Ticks down and reset the timers that determines when the alien sound is changed.
// The sound is actually changed in FleetDelayExShip()
static void TimeFleetSound()
{
// xref 1740
if (--m.fleetSndHold == 0)
DisableFleetSound();
if (m.playerOK == 0)
{
DisableFleetSound();
return;
}
if (--m.fleetSndCnt != 0)
return;
write_port(5, m.soundPort5);
if (m.numAliens == 0)
{
DisableFleetSound();
return;
}
m.fleetSndCnt = m.fleetSndReload;
m.changeFleetSnd = 0x01;
m.fleetSndHold = 0x04;
}
// Turn off the fleet movement sounds
static void DisableFleetSound()
{
// xref 176d
SetSoundWithoutFleet(m.soundPort5);
}
// Mask fleet movement sound off from given byte, and use it to set sound
static void SetSoundWithoutFleet(uint8_t v)
{
// xref 1770
write_port(5, v & 0x30);
}
// Handles rotating the fleet sounds if it is time to do so, and determines the
// delay between them using a table keyed by the number of live aliens.
// The bonus ship sound is also handled here.
static uint8_t FleetDelayExShip()
{
// xref 1775
// The two sound tables (in decimal):
// [ 50 43 36 28 22 17 13 10 08 07 06 05 04 03 02 01 ] (soundDelayKey)
// [ 52 46 39 34 28 24 21 19 16 14 13 12 11 09 07 05 ] (soundDelayValue)
uint8_t snd_b = 0;
uint8_t snd_a = 0;
if (m.changeFleetSnd)
{
uint8_t n = m.numAliens;
// xref 1785
int i = 0;
for (i = 0; i < 16; ++i)
{
if (n >= m.soundDelayKey[i])
break;
}
m.fleetSndReload = m.soundDelayValue[i];
snd_b = m.soundPort5 & 0x30; // Mask off all fleet movement sounds
snd_a = m.soundPort5 & 0x0f; // Mask off all except fleet sounds
// Rotate to next sound and wrap if neccessary
snd_a = (snd_a << 1) | (snd_a >> 7);
if (snd_a == 0x10)
snd_a = 0x01;
m.soundPort5 = snd_a | snd_b;
m.changeFleetSnd = 0;
}
// xref 17aa
if (--m.extraHold == 0)
SoundBits3Off(0xef);
return snd_a;
}
// Read the input port corresponding to the current player.
static uint8_t ReadInputs()
{
// xref 17c0
uint8_t port = m.playerDataMSB & 0x1 ? 1 : 2;
return read_port(port);
}
// End the game if tilt switch is activated.
static void CheckHandleTilt()
{
// xref 17cd
if (!(read_port(2) & TILT_BIT)) return;
if (m.tilt) return;
yield(YIELD_TILT);
assert(FALSE);
}
// This routine is entered after the stack reset in CheckHandleTilt
// It prints the tilt message and ends the game, cycling back to the splash screen.
static void on_tilt()
{
// xref 17dc
for (size_t i = 0; i < 4; ++i)
ClearPlayField();
m.tilt = 1;
DsableGameTasks();
enable_interrupts();
PrintMessageDel(xytosc(96,176), m.MSG_TILT, 4);
OneSecDelay();
m.tilt = 0;
m.waitStartLoop = 0;
GameOver(); // does not return.
assert(FALSE);
}
// Play appropriate sounds based on saucer state.
static void CtrlSaucerSound()
{
// xref 1804
if (m.saucerActive == 0)
{
SoundBits3Off(0xfe);
return;
}
if (m.saucerHit)
return;
SoundBits3On(0x01);
}
// Draws the text and sprites for the "SCORE ADVANCE TABLE" in the splash screens.
static void DrawAdvTable()
{
// xref 1815
PrintMessage(xytosc(32,128), m.MSG__SCORE_ADVANCE_TABLE_, 0x15);
// PrintMessageAdv uses this
m.temp206C = 10;
PriCursor cursor;
// Sprite display list for score advance table
cursor.src = m.SCORE_ADV_SPRITE_LIST;
while (!ReadPriStruct(&cursor))
Draw16ByteSprite(cursor.sc, cursor.obj);
// Message display list for score advance table
cursor.src = m.SCORE_ADV_MSG_LIST;
while (!ReadPriStruct(&cursor))
PrintMessageAdv(cursor.sc, cursor.obj);
}
// Used when drawing the Score Advance Table to draw the alien and saucer sprites
static void Draw16ByteSprite(Word sc, uint8_t* sprite)
{
// xref 1844
SprDesc desc;
desc.sc = sc;
desc.spr = ptr_to_word(sprite);
desc.n = 16;
DrawSimpSprite(&desc);
}
// Used when drawing the Score Advance Table to draw the text
static void PrintMessageAdv(Word sc, uint8_t* msg)
{
// xref 184c
size_t n = m.temp206C;
PrintMessageDel(sc, msg, n);
}
// A PriStruct is a display list, containing a list of
// objects to display (either sprites or text), and their position.
// This routine is used to iterate through the list and read each member.
static int ReadPriStruct(PriCursor *pri)
{
// xref 1856
pri->sc.l = *pri->src++;
if (pri->sc.l == 0xff)
return TRUE; // hit end
pri->sc.h = *pri->src++;
Word obj;
obj.l = *pri->src++;
obj.h = *pri->src++;
pri->obj = word_to_ptr(obj);
return FALSE; // keep going
}
// Required for CCOIN and PLAy splash animations
// The animation structure used here is set up by IniSplashAni()
// Moves and animates a sprite until it reaches a target position
static void SplashSprite()
{
// xref 1868
++m.splashAnForm;
uint8_t* vptr = &m.splashDelta.y;
uint8_t dy = *vptr;
uint8_t x = AddDelta(vptr, dy);
if (m.splashTargetX != x)
{
uint8_t flip = m.splashAnForm & 0x04;
Word spr = m.splashImRest;
if (flip == 0) { spr.u16 += 48; }
m.splashPic = spr;
SprDesc desc = ReadDesc((SprDesc*) u16_to_ptr(SPLASH_DESC_ADDR));
// splash desc is out of order, needs swapping.
Word tmp = desc.pos;
desc.pos = desc.spr;
desc.spr = tmp;
DrawSprite(&desc);
return;
}
// xref 1898
m.splashReached = 1;
}
// Handles the shooting part of the CCOIN splash animation
// Companion routine is SplashSquiggly()
static void AnimateShootingSplashAlien()
{
// xref 189e
BlockCopy(u16_to_ptr(SQUIGGLY_SHOT_ADDR), m.SPLASH_SHOT_OBJDATA, 16);
m.shotSync = 2;
m.alienShotDelta = 0xff;
m.isrSplashTask = 4;
// spin until shot collides
while ((m.squShotData.Status & SHOT_BLOWUP) == 0)
{
// xref 18b8
timeslice(); // spin
}
// spin until shot explosion finishes
while ((m.squShotData.Status & SHOT_BLOWUP) != 0)
{
// xref 18c0
timeslice(); // spin
}
// replace extra 'C' with blank space
DrawChar(xytosc(120,136), 0x26);
TwoSecDelay();
}
// Handle initialization and splash screens.
// Initially entered upon startup via reset(), with skip=0
// Is is terminated when the stack is reset upon the insertion of credits, and is
// replaced by WaitForStart()
// The routine is eventually re-entered with skip=0 via GameOver()
static void main_init(int skip)
{
// xref 18d4
int init = !skip;
if (init)
{
CopyRomtoRam();
DrawStatus();
}
// 18df
while (TRUE)
{
if (init)
{
m.aShotReloadRate = 8;
write_port(3, 0); // turn off sound 1
write_port(5, 0); // turn off sound 2
// SetISRSplashTask
m.isrSplashTask = 0;
// xref 0af2
enable_interrupts();
OneSecDelay();
// xref 0af6
PrintMessageDel(xytosc(96, 184),
m.splashAnimate ? m.MSG_PLAY : m.MSG_PLAY2,
4);
// xref 0b08
MessageToCenterOfScreen(m.MSG_SPACE__INVADERS);
// xref 0b0e
OneSecDelay();
DrawAdvTable();
TwoSecDelay();
// xref 0b17
if (m.splashAnimate == 0)
{
// run script for alien twiddling with upside down 'Y'
IniSplashAni(u16_to_ptr(0x1a95));
Animate();
IniSplashAni(u16_to_ptr(0x1bb0));
Animate();
OneSecDelay();
IniSplashAni(u16_to_ptr(0x1fc9));
Animate();
OneSecDelay();
ClearSmallSprite(xytosc(125, 184), 10, 0);
TwoSecDelay();
}
// xref 0b4a
ClearPlayField();
if (m.p1ShipsRem == 0)
{
// xref 0b54
m.p1ShipsRem = GetShipsPerCred();
RemoveShip();
}
// xref 0b5d
CopyRAMMirror();
InitAliensP1();
DrawShieldP1();
RestoreShieldsP1();
m.isrSplashTask = 1;
DrawBottomLine();
// xref 0b71
do
{
PlrFireOrDemo();
// xref 0b74
// check player shot, and aliens bumping screen, also handle hidden message
uint8_t a2 = CheckPlyrShotAndBump();
// feed the watchdog
write_port(6, a2);
} while (IsPlayerAlive());
// xref 0b7f
m.plyrShotStatus = 0;
// wait for demo player to finish exploding.
while (! IsPlayerAlive() )
{
// xref 0b83
timeslice(); // spin
}
}
// xref 0b89
m.isrSplashTask = 0;
OneSecDelay();
ClearPlayField();
PrintMessage(xytosc(64,136), m.MSG_INSERT__COIN, 12);
// draw extra 'C' for CCOIN
if (m.splashAnimate == 0)
DrawChar(xytosc(120,136), 2);
PriCursor cursor;
cursor.src = m.CREDIT_TABLE;
ReadPriStruct(&cursor);
PrintMessageAdv(cursor.sc, cursor.obj);
// Only print coin info if DIP7 is set
if ((read_port(2) & DIP7_COININFO) == 0)
{
cursor.src = m.CREDIT_TABLE_COINS;
// xref 183a
while (!ReadPriStruct(&cursor))
PrintMessageAdv(cursor.sc, cursor.obj);
}
TwoSecDelay();
// xref 0bc6
if (m.splashAnimate == 0)
{
// xref 0bce
// shoot C animation
IniSplashAni(u16_to_ptr(0x1fd5));
Animate();
AnimateShootingSplashAlien();
}
// xref 0bda
m.splashAnimate = !m.splashAnimate;
ClearPlayField();
}
}
// Return pointer to non current player's alive status
static uint8_t* GetOtherPlayerAliveFlag()
{
// xref 18e7
return (m.playerDataMSB & 0x1 ? &m.playerStates.h : &m.playerStates.l);
}
// Use a mask to enable sounds on port 3
static void SoundBits3On(uint8_t mask)
{
// xref 18fa
uint8_t snd = m.soundPort3;
snd |= mask;
m.soundPort3 = snd;
write_port(3, snd);
}
// Init all the aliens for P2
static void InitAliensP2()
{
// xref 1904
InitAliensSub(u16_to_ptr(P2_ADDR));
}
// Called from the main thread to do some miscellaneous tasks
// a) Player shot collision response
// b) Detecting and handling the alien rack bumping the screen edges
static void PlyrShotAndBump()
{
// xref 190a
PlayerShotHit();
RackBump();
}
// Return pointer to current player's alive status
static uint8_t* CurPlyAlive()
{
// xref 1910
return (m.playerDataMSB & 0x1 ? &m.playerStates.l : &m.playerStates.h);
}
// Draw the score text labels at the top of the screen
static void DrawScoreHead()
{
// xref 191a
PrintMessage(xytosc(0,240), m.MSG__SCORE_1__HI_SCORE_SCORE_2__, 28);
}
// Draw the score for P1
static void PrintP1Score()
{
// xref 1925
DrawScore(&m.P1Scor.l);
}
// Draw the score for P2
static void PrintP2Score()
{
// xref 192b
DrawScore(&m.P2Scor.l);
}
// Draw the score using the given descriptor in iter
static void DrawScore(uint8_t* iter)
{
// xref 1931
Word pos;
Word val;
val.l = *iter++;
val.h = *iter++;
pos.l = *iter++;
pos.h = *iter++;
Print4Digits(pos, val);
}
// Draw the credit text label at the bottom right
static void PrintCreditLabel()
{
// xref 193c
PrintMessage(xytosc(136,8), m.MSG_CREDIT_, 7);
}
// Draw the number of credits at the bottom right
static void DrawNumCredits()
{
// xref 1947
DrawHexByte(xytosc(192,8), m.numCoins);
}
// Draw the high score
static void PrintHiScore()
{
// xref 1950
DrawScore(&m.HiScor.l);
}
// Clear the screen and draw all the text surrounding the playfield
static void DrawStatus()
{
// xref 1956
ClearScreen();
DrawScoreHead();
PrintP1Score();
PrintP2Score();
PrintHiScore();
PrintCreditLabel();
DrawNumCredits();
// Midway patched this out
// PrintTaitoCorporation();
}
// Prints "* TAITO CORPORATION *" at the bottom of the screen
// This is a bit of dead code due to the patching out in DrawStatus()
static void PrintTaitoCorporation()
{
// xref 198b
PrintMessage(xytosc(32, 24), u16_to_ptr(0x19be), 0x13);
}
// Called during the game demo to check if player has entered the correct
// button combos to display the easter egg, "TAITO COP"
static void CheckHiddenMes()
{
// xref 199a
uint8_t a = 0;
if (m.hidMessSeq == 0)
{
a = read_port(1);
a &= 0x76;
a -= 0x72;
if (a)
return;
m.hidMessSeq = 1;
}
a = read_port(1);
a &= 0x76;
a -= 0x34;
if (a)
return;
PrintMessage(xytosc(80,216), m.MSG_TAITO_COP, 9);
}
// Allow game related tasks in interrupt routines
static void EnableGameTasks()
{
// xref 19d1
m.gameTasksRunning = 1;
}
// Disallow game related tasks in interrupt routines
static void DsableGameTasks()
{
// xref 19d7
m.gameTasksRunning = 0;
}
// Use a mask to turn off sounds in port 3
static void SoundBits3Off(uint8_t mask)
{
// xref 19dc
uint8_t snd = m.soundPort3;
snd &= mask;
m.soundPort3 = snd;
write_port(3, snd);
}
// Draw the sprites representing the number of lives (0 based) remaining at the bottom left
static void DrawNumShips(uint8_t n)
{
// xref 19e6
SprDesc desc;
desc.sc = xytosc(24,8);
desc.spr = ptr_to_word(m.PLAYER_SPRITES);
desc.n = 16;
for (int i = 0; i < n; ++i)
desc.sc = DrawSimpSprite(&desc);
DrawNumShipsSub(desc.sc);
}
// Clears the space to the right of the ship sprites (used to remove lives)
static void DrawNumShipsSub(Word pos)
{
// xref 19fa
// Clear up to x = 136 (start of credit label)
do
{
pos = ClearSmallSprite(pos, 16, 0);
} while ((pos.x != 0x35));
}
// Returns true if given obj is positioned on the half of screen that is not currently being drawn.
// Used to control which interrupt draws a particular game object.
// This prevents flicker.
static uint8_t CompXrToBeam(uint8_t* posaddr)
{
// xref 1a06
uint8_t b = m.vblankStatus;
uint8_t a = *posaddr & XR_MID;
return a == b;
}
// memcpy
static void BlockCopy(void *_dest, void *_src, size_t n)
{
// xref 1a32
uint8_t* dest = (uint8_t*) (_dest);
uint8_t* src = (uint8_t*) (_src);
for (size_t i = 0; i < n; ++i)
*dest++ = *src++;
}
// Return a copy of a Sprite Descriptor from src
static SprDesc ReadDesc(SprDesc* src)
{
// xref 1a3b
SprDesc desc;
desc.spr.l = src->spr.l;
desc.spr.h = src->spr.h;
desc.pos.l = src->pos.l;
desc.pos.h = src->pos.h;
desc.n = src->n;
return desc;
}
// Convert a pixel pos to a screen address
// Pixel positions in memory are pre-offset by +32 pixels, meaning that when
// they are converted to a screen coordinate, 0x400 has already been added.
// Hence the or with 0x2000 below, instead of +0x2400
// See xpix()
static Word ConvToScr(Word pos)
{
// xref 1a47
return u16_to_word((pos.u16 >> 3) | 0x2000);
}
// bzero the 7168 bytes of vram
static void ClearScreen()
{
// xref 1a5c
uint8_t* screen = m.vram;
size_t n = 7168;
for (size_t i = 0; i < n; ++i)
screen[i] = 0;
}
// CopyShields() subroutine.
// cursor - used to iterate through the screen and the player buffer
// spr_size - contains the number of rows and bytes per row of the sprite
// dir=0 - Copy a shield from the buffer to the screen
// dir=1 - Copy a shield from the screen to the buffer
static void CopyShieldBuffer(ShieldBufferCursor *cursor, Word spr_size, uint8_t dir)
{
// xref 1a69
uint8_t nr = spr_size.h;
uint8_t nb = spr_size.l;
for (int i = 0; i < nr; ++i, cursor->sc.u16 += xysc(1,0))
{
uint8_t* screen = word_to_ptr(cursor->sc);
for (int j = 0; j < nb; ++j)
{
if (dir == 0)
*screen = *cursor->iter | *screen;
else
*cursor->iter = *screen;
cursor->iter++;
screen++;
}
}
}
// Take a life away from the player, and update the indicators
static void RemoveShip()
{
// xref 1a7f
uint8_t* nships_ptr;
uint8_t num = GetNumberOfShips(&nships_ptr);
if (num == 0)
return;
*nships_ptr = num-1;
// The sprite indicator is 0 based.
DrawNumShips(num-1);
// The text indicator is 1 based.
DrawHexByteSub(xytosc(8,8), num & 0xf);
}
// Print the numeric lives indicator at the bottom left
static void PrintNumShips(uint8_t num)
{
// xref 1a8b
DrawChar(xytosc(8,8), (num & 0x0f) + 0x1a);
}
// GAMECODE FINISH