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