/* game.m,
 *
 * The game loop.  Filthy, but it works.  Note some variables may be
 * inside common.m because it's more useful there.
 */

#include <allegro.h>
#include <assert.h>
#include <ctype.h>			/* High scores. */
#include <math.h>
#include <objc/Object.h>
#ifndef NO_FBLEND
# include "fblend/include/fblend.h"
#endif
#include "common.h"
#include "debri.h"
#include "end-level.h"
#include "game.h"
#include "group.h"
#include "hiscore.h"
#include "input.h"
#include "map-save.h"
#include "map.h"
#include "music-ad.h"
#include "music.h"
#include "newmenu-log.h"
#include "newmenu.h"
#include "nuke.h"
#include "player-stats.h"
#include "player.h"
#include "powerup.h"
#include "projectile.h"
#include "scramble-letter.h"
#include "seborrhea/seborrhea-allegro.h"
#include "seborrhea/seborrhea-font.h"
#include "sound.h"
#include "star-field.h"
#include "unit.h"
#include "units/all-units.h"


#define STATUS_BITMAP_W		80
#define STATUS_BITMAP_H		185 /*165*/
#define STATUS_BITMAP_X1	((((640-screen_w)/2) - STATUS_BITMAP_W)/2)
#define STATUS_BITMAP_X2	(screen_w+status_w + STATUS_BITMAP_X1)
#define STATUS_BITMAP_Y		200

#ifndef NO_VIDEO_BITMAPS
BOOL raid_use_page_flipping = YES;
BOOL raid_use_triple_buffering = YES;
static BITMAP *page[3], *active_page;
static int active_page_n;
#endif

static SebFont *main_font, *name_font;
static BITMAP *real_dbuf;
static BITMAP *dbuf, *left_status, *right_status; /* Sub-bitmaps. */
static StarField *star_field;

static void blit_to_screen(void);
static void game_loop_init(void);
static void game_loop_shutdown(void);

/*--------------------------------------------------------------*/
/* FPS.								*/
/*--------------------------------------------------------------*/

static volatile int counter;
static void ticker(void) { counter++; }

static volatile int fps, prev_fps;
static void fps_ticker(void) { prev_fps = fps; fps = 0; }

static void draw_fps(BITMAP *dest)
{
    int c = makecol(0xff, 0x80, 0x80);
    fps++;

    textprintf(dest, font, 0, 0, c, "FPS: %d", prev_fps);

#ifndef NO_VIDEO_BITMAPS
    if (raid_use_triple_buffering)
	textout(dest, font, "Triple buffering", 0, 10, c);	
    else if (raid_use_page_flipping)
	textout(dest, font, "Page flipping",    0, 10, c);
    else
	textout(dest, font, "Double buffering", 0, 10, c);
#endif

#ifndef NO_FBLEND
    textout(dest, font, "FBlend", 0, 20, c);
#endif

#ifdef WINFF
    textout(dest, font, "Force Feedback", 0, 30, c);
#endif
}

static BOOL toggle_fps(void)
{
    static BOOL held;
    BOOL was_held = held;
    held = key[KEY_F12];

    return (held && !was_held);
}

/*--------------------------------------------------------------*/

static BOOL toggle_shadows(void)
{
    static BOOL held;
    BOOL was_held = held;
    held = key[KEY_F11];

    return (held && !was_held);
}

/*--------------------------------------------------------------*/

static void grayout_bitmap(BITMAP *bmp)
{
    int gray = makecol(0x40, 0x40, 0x40);

#ifndef NO_FBLEND
    fblend_rect_trans(bmp, 0, 0, bmp->w, bmp->h, gray, 0x80);
#else  /* Normal Allegro routines. */
    drawing_mode(DRAW_MODE_TRANS, NULL, 0, 0);
    set_trans_blender(0xff, 0xff, 0xff, 0x80);
    rectfill(bmp, 0, 0, bmp->w-1, bmp->h-1, gray);
    drawing_mode(DRAW_MODE_SOLID, NULL, 0, 0);
#endif /* NO_FBLEND. */
}

static void blit_sidebars(BITMAP *blitme, int x)
{
#define DRAW_SIDEBAR_TO(dest)		blit(blitme,dest,0,0,x,0,blitme->w,blitme->h)

#ifndef NO_VIDEO_BITMAPS
    if (raid_use_page_flipping || raid_use_triple_buffering) {
	DRAW_SIDEBAR_TO(page[0]);
	DRAW_SIDEBAR_TO(page[1]);
	if (raid_use_triple_buffering)
	    DRAW_SIDEBAR_TO(page[2]);
	return;
    }
#endif

    DRAW_SIDEBAR_TO(screen);

#undef DRAW_SIDEBAR_TO
}

static void redraw_sidebars(void)
{
    /* To avoid a BIG (REALLY BIG) speed hit, we only draw the left
       and right sidebars at the beginning of the game, and when we
       lose focus and come back (eg. alt-tabbing away). */

    Sebum<SebImage> *sidebar = [base_sebum getSebumByName:"glue/sidebar"];
    BITMAP *blitme = create_bitmap((SCREEN_W-screen_w)/2, SCREEN_H);

    if (!sidebar || !blitme)
	return;

    [sidebar drawTo:blitme X:0 Y:0 W:blitme->w H:blitme->h];

    /* Left sidebar. */
    blit(left_status, blitme, 0, 0, STATUS_BITMAP_X1, STATUS_BITMAP_Y, STATUS_BITMAP_W, STATUS_BITMAP_H);
    blit_sidebars(blitme, 0);

    /* Right sidebar.  Note X coordinate is correct. */
    blit(right_status, blitme, 0, 0, STATUS_BITMAP_X1, STATUS_BITMAP_Y, STATUS_BITMAP_W, STATUS_BITMAP_H);
    blit_sidebars(blitme, SCREEN_W-blitme->w);

    FREE_BITMAP(blitme);
}

#ifndef NO_ID3TAGS
static void redraw_sidebar_for_music_ad(void)
{
    Sebum<SebImage> *sidebar = [base_sebum getSebumByName:"glue/sidebar"];

    if (sidebar) {
	[sidebar drawFromX:0 Y:SCREEN_H-54
		 To:real_dbuf X:0 Y:SCREEN_H-54 W:[sidebar width] H:54];
    }
}
#endif

/*--------------------------------------------------------------*/

static void draw_level_complete(BITMAP *dest)
{
    draw_letters(dest, level_complete_letters);
}

static void draw_game_over(BITMAP *dest)
{
    /* Game_Over man, GAME_OVER! */
    grayout_bitmap(dest);
    draw_letters(dest, game_over_letters);

    [main_font putString:"Press START to retry" To:dbuf X:dbuf->w/2 Y:dbuf->h/2+20 Colour:0xe0:0xe0:0xff Alignment:ALIGN_CENTRE Decoration:DECORATION_OUTLINE];
    [main_font putString:"or ESCAPE to quit"    To:dbuf X:dbuf->w/2 Y:dbuf->h/2+40 Colour:0xe0:0xe0:0xff Alignment:ALIGN_CENTRE Decoration:DECORATION_OUTLINE];
}

static void draw_statusbars(void)
{
    if (player[0])
	[player[0] drawHealth:left_status];
    else if (game_flags & FLAG_REDRAW_STATUSBARS)
	draw_death_statusbar(left_status, 0);

    if (player[1])
	[player[1] drawHealth:right_status];
    else if (num_players > 1 && game_flags & FLAG_REDRAW_STATUSBARS)
	draw_death_statusbar(right_status, 1);
}

/*--------------------------------------------------------------*/
/* Screenshots (F10).						*/
/*--------------------------------------------------------------*/

static void take_screenshot(BITMAP *bmp)
{
    static int screenshot_num = 0;
    char filename[16];
    snprintf(filename, sizeof filename, "shot%04d.bmp", screenshot_num);
    save_bitmap(filename, bmp, NULL);
    screenshot_num++;
}

/*--------------------------------------------------------------*/
/* Pause.							*/
/*--------------------------------------------------------------*/

static void do_game_paused_update(void) 
{
    if (start_pressed())
	game_flags ^= FLAG_GAME_PAUSED;
    if (menu_released())
	game_flags |= FLAG_QUIT;
    rest(0);				/* Yield! */
}

static void draw_game_paused(BITMAP *dest)
{
    Sebum<SebImage> *spr;
    grayout_bitmap(dest);

    spr = [base_sebum getSebumByName:"glue/paused"];
    [spr drawTo:dest X:(dest->w-[spr width])/2 Y:200];
    [main_font putString:"Press START to resume" To:dbuf X:dbuf->w/2 Y:dbuf->h/2+20 Colour:0xe0:0xe0:0xff Alignment:ALIGN_CENTRE Decoration:DECORATION_OUTLINE];
    [main_font putString:"or ESCAPE to quit"     To:dbuf X:dbuf->w/2 Y:dbuf->h/2+40 Colour:0xe0:0xe0:0xff Alignment:ALIGN_CENTRE Decoration:DECORATION_OUTLINE];
}

/*--------------------------------------------------------------*/
/* High score.							*/
/*--------------------------------------------------------------*/

#define HIGH_SCORE_NAME_LEN		20
#ifdef CHEAT
static char high_score_name_buf1[HIGH_SCORE_NAME_LEN+1] = "Cheater!";
static char high_score_name_buf2[HIGH_SCORE_NAME_LEN+1] = "Cheater!";
#else
static char high_score_name_buf1[HIGH_SCORE_NAME_LEN+1] = "Nameless";
static char high_score_name_buf2[HIGH_SCORE_NAME_LEN+1] = "Bob";
#endif
static char current_high_score_char = 'a';
static int backspace_repeat_delay;

static void enter_high_score(int pl)
{
    int scroll_direction = 0;
    char *name = (pl == 0) ? high_score_name_buf1 : high_score_name_buf2;

    if (start_released()) {		/* Done typing. */
	update_high_score(name, player_stats[pl].score, SOURCE_UNKNOWN);
	if (pl == 0)
	    game_flags &=~FLAG_PLAYER1_HIGH_SCORE;
	else if (pl == 1)
	    game_flags &=~FLAG_PLAYER2_HIGH_SCORE;
	return;
    }

    if (keypressed()) {			/* Keyboard. */
	int c = (readkey() & 0xff);
	int l = strlen(name);

	if (not isprint(c))
	    return;

	if (l < HIGH_SCORE_NAME_LEN) {
	    name[l] = c;
	    name[l+1] = '\0';
	    l++;
	}
    }

    if (key[KEY_BACKSPACE]) {
	if (backspace_repeat_delay <= 0) {
	    backspace_repeat_delay = 15;
	    goto delete_character;
	}
	else
	    backspace_repeat_delay--;
    }
    else
	backspace_repeat_delay = 0;

    if (key[KEY_U] && key_shifts & KB_CTRL_FLAG) {
	name[0] = '\0';
	return;
    }

    if (player_controls[pl].joystick) {	/* Joystick. */
	if ([player_controls[pl].joystick pressed:RAID_KEY_UP])
	    scroll_direction++;
	if ([player_controls[pl].joystick pressed:RAID_KEY_DOWN])
	    scroll_direction--;
	if ([player_controls[pl].joystick pressed:RAID_KEY_FIRE])
	    goto append_character;
	if ([player_controls[pl].joystick pressed:RAID_KEY_MENU] ||
	    [player_controls[pl].joystick pressed:RAID_KEY_LEFT])
	    goto delete_character;
    }

    if (scroll_direction) {
	current_high_score_char += scroll_direction;

	if (current_high_score_char < ' ')
	    current_high_score_char = ' ';

	if (current_high_score_char > '~')
	    current_high_score_char = '~';
    }

    return;

 append_character:
    {
	int l = strlen(name);
	if (l < HIGH_SCORE_NAME_LEN) {
	    name[l] = current_high_score_char;
	    name[l+1] = '\0';
	}
	return;
    }

 delete_character:
    {
	int l = strlen(name);
	if (l > 0)
	    name[l-1] = '\0';
	return;
    }
}

static void draw_high_score(BITMAP *dest, int pl)
{
    static int t;
    int l;
    char *name = (pl == 0) ? high_score_name_buf1 : high_score_name_buf2;
    char str[2];

    grayout_bitmap(dest);
    draw_letters(dest, high_score_letters);

    [main_font putString:"Enter your name" To:dest X:dest->w/2 Y:dest->h/2+10
	       Colour:0xb0:0xb0:0xb0 Alignment:ALIGN_CENTRE
	       Decoration:DECORATION_OUTLINE];

    [main_font putStringTo:dest X:dest->w/2 Y:dest->h/2+40
	       Colour:0xa0:0x60:0xe0 Alignment:ALIGN_CENTRE
	       Decoration:DECORATION_OUTLINE :"Player %d", pl+1];

    l = [name_font textLength:name];
    str[0] = current_high_score_char;
    str[1] = '\0';
    [name_font putString:name To:dest X:(dest->w-l)/2 Y:dest->h/2+70
	       Colour:0xb0:0xb0:0xb0 Decoration:DECORATION_OUTLINE];

    if (++t > 30)
	t = 0;

    if (t > 15) {		/* Dark */
	rectfill_wh(dest, (dest->w+l)/2+1, dest->h/2+70,
		    [name_font textLength:str]+4, 20, makecol(0xe0,0xa0,0x60));
	[name_font putString:str To:dest X:(dest->w+l)/2+3 Y:dest->h/2+70
		   Colour:0x40:0x40:0x40 Decoration:DECORATION_OUTLINE];
    }
    else {
	rectfill_wh(dest, (dest->w+l)/2+1, dest->h/2+70,
		    [name_font textLength:str]+4, 20, makecol(0x40,0x40,0x40));
	[name_font putString:str To:dest X:(dest->w+l)/2+3 Y:dest->h/2+70
		   Colour:0xe0:0xa0:0x60 Decoration:DECORATION_OUTLINE];
    }
}

static void finish_campaign_high_score_loop(void)
{
    BOOL redraw = YES;
    scramble_letters(high_score_letters);

    while ((game_flags & FLAG_PLAYER1_HIGH_SCORE) ||
	   (game_flags & FLAG_PLAYER2_HIGH_SCORE)) {
	while (counter) {
	    counter--;
	    poll_raid_keys(num_players);
	    if (game_flags & FLAG_PLAYER1_HIGH_SCORE)
		enter_high_score(0);
	    else if (game_flags & FLAG_PLAYER2_HIGH_SCORE)
		enter_high_score(1);
	    unscramble_letters(high_score_letters);
	    redraw = YES;
	}

	if (redraw) {
	    redraw = NO;
	    clear_to_color(dbuf, makecol(0x40, 0x40, 0x40));

	    if (game_flags & FLAG_PLAYER1_HIGH_SCORE)
		draw_high_score(dbuf, 0);
	    else if (game_flags & FLAG_PLAYER2_HIGH_SCORE)
		draw_high_score(dbuf, 1);

	    if (game_flags & FLAG_REDRAW_SIDEBARS) {
		game_flags &=~FLAG_REDRAW_SIDEBARS;
		redraw_sidebars();
	    }

	    blit_to_screen();
	}
    }
}

/*--------------------------------------------------------------*/

static BOOL raid_reset(const char *map_fn)
{
    int map_length;

    /* Reset our lists. */
    unit_reset();
    unit_group_shutdown();	unit_group_init();
    projectile_shutdown();	projectile_init();

    if (not load_map(map_fn, NO))
	return NO;

    /* The map doesn't rely on these, so we can delay. */
    debris_shutdown();		debris_init();
    nuke_shutdown();		nuke_init();
    powerup_shutdown();		powerup_init();

    map_length = TILE_H * [current_map length];
    [current_map setOffsetX:0 Y:map_length-screen_h];

    if (num_players == 1) {
	player[0] = (Player *)spawn_unit([Player class],  screen_w*1/2, map_length, ALLY_LIST, YES);
	player[1] = nil;
    }
    else {			/* Two-players. */
	player[0] = (Player *)spawn_unit([Player class],  screen_w*1/3, map_length, ALLY_LIST, YES);
	player[1] = (Player *)spawn_unit([Player2 class], screen_w*2/3, map_length, ALLY_LIST, YES);
    }

    /* The map wants stars, so make a star field. */
    if ([current_map stars])
	star_field = [StarField new];

    game_flags &=~FLAG_LEVEL_COMPLETE;
    game_flags |= FLAG_PLAYERS_ALIVE;

    return YES;
}

static void do_game_update(const char *level_name, BOOL check_high_score)
{
#ifndef NO_ID3TAGS
    if (music_ad)
	music_ad = [music_ad update];
#endif

    if ((game_flags & FLAG_PLAYERS_ALIVE) &&
	not player[0] && not player[1]) {
	/* Just died recently. */
	game_flags &=~FLAG_PLAYERS_ALIVE;
	scramble_letters(game_over_letters);
	scramble_letters(high_score_letters);
	clear_keybuf();
    }

    if (game_flags & FLAG_LEVEL_COMPLETE && not boss) {
	if (player[0]) [player[0] zoom];
	if (player[1]) [player[1] zoom];

	if (not (game_flags & (FLAG_PLAYER1_HIGH_SCORE|FLAG_PLAYER2_HIGH_SCORE))) {
	    unscramble_letters(level_complete_letters);
	    end_level_tics--;
	}
    }
    else
	do_input();

    if (toggle_shadows())		/* F11: toggle shadow. */
	game_flags ^= FLAG_DRAW_SHADOW;
    if (toggle_fps())			/* F12: toggle FPS. */
	game_flags ^= FLAG_DRAW_FPS;

    if (star_field)
	[star_field update];

    [current_map update];
    update_debris();
    update_powerups();
    update_nukes();
    update_projectiles();
    update_units();

    if (not (game_flags & FLAG_PLAYERS_ALIVE)) { /* Restart/Quit. */
	if (not check_high_score) {
	    game_flags &=~FLAG_PLAYER1_HIGH_SCORE;
	    game_flags &=~FLAG_PLAYER2_HIGH_SCORE;
	}

	if (game_flags & FLAG_PLAYER1_HIGH_SCORE) {
	    unscramble_letters(high_score_letters);
	    enter_high_score(0);
	}
	else if (game_flags & FLAG_PLAYER2_HIGH_SCORE) {
	    unscramble_letters(high_score_letters);
	    enter_high_score(1);
	}

	/* Don't restart if we died but killed the boss. */
	else if (game_flags & FLAG_LEVEL_COMPLETE)
	    return;

	else if (menu_released())
	    game_flags |= FLAG_QUIT;
	else if (retry_released()) {
	    raid_reset(level_name);
	    free_reserved_sounds();
	    game_flags |= FLAG_PLAYERS_ALIVE;

	    /* Don't put this inside raid_reset() because we want
	       score to carry to the next level. */
	    clear_stats_for_player(0);
	    clear_stats_for_player(1);
	}
	else {
	    unscramble_letters(game_over_letters);
	}
    }
    else if (not (game_flags & FLAG_LEVEL_COMPLETE)) { /* Pause game. */
	if (start_pressed() || menu_released())
	    game_flags ^= FLAG_GAME_PAUSED;
	else if (join_pressed()) {
	    /* Give player 2 a go too! */
	    num_players = 2;
	    player[1] = (Player *)spawn_unit([Player2 class], screen_w*2/3, offsetY+screen_h, ALLY_LIST, YES);
	}
    }
}

/*--------------------------------------------------------------*/

#ifndef NO_VIDEO_BITMAPS
static int next_active_page(int curr)
{
    int next = curr + 1;

    if ((!raid_use_triple_buffering && next > 1) || (next > 2))
	return 0;
    return next;
}
#endif

static void blit_to_screen(void)
{
#define BLIT_LEFT_STATUSBAR_TO(bmp)	blit(left_status, bmp,0,0,STATUS_BITMAP_X1,STATUS_BITMAP_Y,STATUS_BITMAP_W,STATUS_BITMAP_H)
#define BLIT_RIGHT_STATUSBAR_TO(bmp)	blit(right_status,bmp,0,0,STATUS_BITMAP_X2,STATUS_BITMAP_Y,STATUS_BITMAP_W,STATUS_BITMAP_H)
#define BLIT_STATUSBARS_TO(bmp)		{ BLIT_LEFT_STATUSBAR_TO(bmp); BLIT_RIGHT_STATUSBAR_TO(bmp); }

#ifndef NO_VIDEO_BITMAPS
    if (raid_use_page_flipping || raid_use_triple_buffering) {
	if (game_flags & FLAG_REDRAW_STATUSBARS) {
	    game_flags &=~FLAG_REDRAW_STATUSBARS;
	    BLIT_STATUSBARS_TO(page[0]);
	    BLIT_STATUSBARS_TO(page[1]);
	    if (raid_use_triple_buffering)
		BLIT_STATUSBARS_TO(page[2]);
	}
	else {
	    if (player[0]) BLIT_LEFT_STATUSBAR_TO(active_page);
	    if (player[1]) BLIT_RIGHT_STATUSBAR_TO(active_page);
	}

#ifndef NO_ID3TAGS
	if (music_ad)
	    blit(real_dbuf, active_page, 0, SCREEN_H-54, 0, SCREEN_H-54, status_w, 54);
#endif

	blit(dbuf, active_page, 0, 0, status_w, 0, dbuf->w, dbuf->h);

	if (raid_use_triple_buffering) {
	    while (poll_scroll());
	    request_video_bitmap(active_page);
	}
	else {
	    show_video_bitmap(active_page);
	}

	active_page_n = next_active_page(active_page_n);
	active_page = page[active_page_n];
	return;
    }
#endif /* NO_VIDEO_BITMAPS */

    /* Double buffering.  Try to draw as little as possible. */
    if (player[0] || game_flags & FLAG_REDRAW_STATUSBARS)
	BLIT_LEFT_STATUSBAR_TO(screen);
    if (player[1] || game_flags & FLAG_REDRAW_STATUSBARS)
	BLIT_RIGHT_STATUSBAR_TO(screen);
    game_flags &=~FLAG_REDRAW_STATUSBARS;

#ifndef NO_ID3TAGS
    if (music_ad)
	blit(real_dbuf, screen, 0, SCREEN_H-54, 0, SCREEN_H-54, status_w, 54);
#endif

    blit(dbuf, screen, 0, 0, status_w, 0, dbuf->w, dbuf->h);

#undef BLIT_STATUSBARS_TO
#undef BLIT_RIGHT_STATUSBAR_TO
#undef BLIT_LEFT_STATUSBAR_TO
}

/*--------------------------------------------------------------*/

enum RAID_LOOP_EXIT_REASON {
    RAID_LOOP_LOAD_MAP_FAILED,
    RAID_LOOP_PLAYER_IS_A_QUITTER,
    RAID_LOOP_LEVEL_ENDED
};

static enum RAID_LOOP_EXIT_REASON raid_loop(const char *level_name,
					    BOOL check_high_score)
{
    enum RAID_LOOP_EXIT_REASON exit_reason = RAID_LOOP_PLAYER_IS_A_QUITTER;
    game_flags = (FLAG_REDRAW|FLAG_REDRAW_SIDEBARS|FLAG_REDRAW_STATUSBARS|
		  FLAG_DRAW_SHADOW);

    if (not raid_reset(level_name)) {
	game_flags |= FLAG_QUIT;
	exit_reason = RAID_LOOP_LOAD_MAP_FAILED;
    }
    else {
	char music_dir[PATH_MAX] = "music/";

	unload_unnecessary_unit_data();
	unload_unnecessary_projectile_data();
	strncat(music_dir, [current_map musicDirname], sizeof music_dir);
	play_music(music_dir);
    }

    counter = 0;
    while (not (game_flags & FLAG_QUIT) &&
	   (not (game_flags & FLAG_LEVEL_COMPLETE) || end_level_tics > 0)) {

	while (counter) {
	    counter--;

	    poll_raid_keys(num_players);

	    if (game_flags & FLAG_GAME_PAUSED) {
		do_game_paused_update();

		if (key[KEY_F10]) {
		    /* Special. */
		    game_flags |= FLAG_REDRAW;
		}
	    }
	    else {
		do_game_update(level_name, check_high_score);
		game_flags |= FLAG_REDRAW;
	    }

#ifdef DEBUG
	    if (key[KEY_PGUP]) [current_map setScrollRate:-0.1 :NO];
	    if (key[KEY_PGDN]) [current_map setScrollRate: 0.1 :NO];
	    if (key[KEY_HOME]) [current_map setScrollRate:DEFAULT_SCROLL_RATE :YES];
	    if (key[KEY_END])  [current_map setScrollRate:0.0 :YES];
#endif
	    /* Music. */
	    poll_music();
	}

	if (game_flags & FLAG_REDRAW) {
            game_flags &=~FLAG_REDRAW;

	    if ([current_map stars]) {	/* Special case for stars. */
		clear_to_color(dbuf, makecol(0x00, 0x00, 0x00));
		[star_field draw:dbuf];
		[current_map drawParallaxLayer:dbuf :YES];
		draw_debris(dbuf, SUBTERRANIAN_LAYER);
	    }

	    {				/* Objects on the ground. */
		[current_map drawTiles:dbuf];
		draw_debris(dbuf, FLOOR_LAYER);
		draw_units(dbuf, ACTIVE_GROUND_LIST);
		draw_debris(dbuf, LOW_LAYER);

		/* Someone found shadows disorienting and asked for
		   this.  Think it was Fenix Blade guy. */
		if (game_flags & FLAG_DRAW_SHADOW)
		    draw_unit_shadows(dbuf);
	    }

	    {				/* Objects in the air. */
		draw_debris(dbuf, MEDIUM_LAYER);
		draw_powerups(dbuf);
		draw_units(dbuf, ACTIVE_AIR_LIST);
		draw_units(dbuf, ALLY_LIST);
		draw_projectiles(dbuf);
		draw_debris(dbuf, HIGH_LAYER);
		draw_nukes(dbuf);
	    }

	    {				/* Stats. */
		if (game_flags & FLAG_REDRAW_SIDEBARS) {
		    game_flags &=~FLAG_REDRAW_SIDEBARS;
		    redraw_sidebars();
		}

		draw_statusbars();
		
		/* Note: boss == nil does not mean the boss is dead! */
		if (boss) [boss drawHealthMeter:dbuf];
	    }

	    {				/* Misc info. */
		if (key[KEY_F10])
		    take_screenshot(dbuf);

		if (game_flags & FLAG_DRAW_FPS) draw_fps(dbuf);

		if (!(game_flags & FLAG_PLAYERS_ALIVE) ||
		    (game_flags & FLAG_LEVEL_COMPLETE)) {
		    if (game_flags & FLAG_PLAYER1_HIGH_SCORE)
			draw_high_score(dbuf, 0);
		    else if (game_flags & FLAG_PLAYER2_HIGH_SCORE)
			draw_high_score(dbuf, 1);
		    else if (game_flags & FLAG_LEVEL_COMPLETE)
			draw_level_complete(dbuf);
		    else
			draw_game_over(dbuf);
		}
		else if (game_flags & FLAG_GAME_PAUSED)
		    draw_game_paused(dbuf);

#ifndef NO_ID3TAGS
		if (music_ad) {
		    redraw_sidebar_for_music_ad();
		    [music_ad drawTo:real_dbuf];
		}
#endif
	    }

	    blit_to_screen();
	}
    }

    /* If a player has died, reset the score. */
    if (not player[0]) clear_stats_for_player(0);
    if (not player[1]) clear_stats_for_player(1);

    free_music();
    free_reserved_sounds();

    debris_shutdown();
    unit_group_shutdown();
    projectile_shutdown();
    nuke_shutdown();
    powerup_shutdown();
    unit_shutdown();
    if (star_field)
	star_field = [star_field free];

    if (game_flags & FLAG_LEVEL_COMPLETE)
	exit_reason = RAID_LOOP_LEVEL_ENDED;

    return exit_reason;
}

void raid_start_levels(const char *level_list[], int num_levels,
		       BOOL check_high_score, BOOL prefix_with_data_dir)
{
    enum RAID_LOOP_EXIT_REASON exit_reason = 0;
    BOOL exit = NO;
    int i, level_n = 0;

    game_loop_init();

    current_map = [Map new];
    assert(current_map);

    clear_stats_for_player(0);
    clear_stats_for_player(1);

    while (not exit && level_n < num_levels) {
	if (prefix_with_data_dir) {
	    for (i = 0; seborrhea_data_root_directories[i]; i++) {
		char fn[PATH_MAX];
		snprintf(fn, sizeof fn, "%s/%s",
			 seborrhea_data_root_directories[i],
			 level_list[level_n]);

		exit_reason = raid_loop(fn, check_high_score);

		/* We loaded the map.  Don't play this level again. */
		if (exit_reason != RAID_LOOP_LOAD_MAP_FAILED)
		    break;
	    }
	}
	else {
	    exit_reason = raid_loop(level_list[level_n], check_high_score);
	}

	if (exit_reason == RAID_LOOP_LOAD_MAP_FAILED) {
	    append_message("Error opening %s!", level_list[level_n]);
	    fprintf(stderr, "Error opening %s!\n", level_list[level_n]);
	    exit = YES;
	}
	else if (exit_reason == RAID_LOOP_PLAYER_IS_A_QUITTER)
	    exit = YES;
	else
	    level_n++;
    }

    current_map = [current_map free];

    /* Special for after finishing the campaign. */
    if (check_high_score && not emergency_exit)
	finish_campaign_high_score_loop();

    game_loop_shutdown();
}

/*--------------------------------------------------------------*/
/* Initialization and shutdown and some more.			*/
/*--------------------------------------------------------------*/

#ifndef NO_VIDEO_BITMAPS
void game_init_video_bitmaps(void)
{
    /* Do we need to create any video bitmaps? */
    if (not (gfx_capabilities & GFX_CAN_SCROLL))
	raid_use_page_flipping = NO;
    if (not (gfx_capabilities & GFX_CAN_TRIPLE_BUFFER))
	raid_use_triple_buffering = NO;

    if (raid_use_page_flipping || raid_use_triple_buffering) {
	page[0] = create_video_bitmap(SCREEN_W, SCREEN_H);
	page[1] = create_video_bitmap(SCREEN_W, SCREEN_H);

	if (not page[0] || not page[1]) {
	    raid_use_page_flipping = NO;
	    raid_use_triple_buffering = NO;
	    fprintf(stderr, "Error creating video memory pages.\n");
	}
	else if (raid_use_triple_buffering) {
	    page[2] = create_video_bitmap(SCREEN_W, SCREEN_H);
	    if (not page[2]) {
		raid_use_triple_buffering = NO;
		fprintf(stderr, "Error creating video memory page.\n");
	    }
	}
    }
}

void game_shutdown_video_bitmaps(void)
{
    FREE_BITMAP(page[0]);
    FREE_BITMAP(page[1]);
    FREE_BITMAP(page[2]);
}
#endif

static void switch_into_game(void)
{
    /* Mark in FLAG_REDRAW in-case we were in paused mode. */
    game_flags |= FLAG_REDRAW|FLAG_REDRAW_SIDEBARS;
}

static void game_loop_init(void)
{
    real_dbuf = create_bitmap(SCREEN_W, SCREEN_H);
    assert(real_dbuf);

    dbuf = create_sub_bitmap(real_dbuf, status_w, 0, screen_w, screen_h);
    left_status  = create_sub_bitmap(real_dbuf, STATUS_BITMAP_X1, STATUS_BITMAP_Y,
				     STATUS_BITMAP_W, STATUS_BITMAP_H);
    right_status = create_sub_bitmap(real_dbuf, STATUS_BITMAP_X2, STATUS_BITMAP_Y,
				     STATUS_BITMAP_W, STATUS_BITMAP_H);
    assert(dbuf && left_status && right_status);

#ifndef NO_VIDEO_BITMAPS
    if (raid_use_page_flipping || raid_use_triple_buffering) {
	show_video_bitmap(page[0]);
	active_page = page[1];
    }
#endif

    {					/* Status backdrops. */
	Sebum<SebImage> *bg = [base_sebum getSebumByName:"glue/statusbar-bg"];
	if (bg) {
	    [bg drawTo:left_status  X:0 Y:0 W:STATUS_BITMAP_W H:STATUS_BITMAP_H];
	    [bg drawTo:right_status X:0 Y:0 W:STATUS_BITMAP_W H:STATUS_BITMAP_H];
	}
    }

    {
	/* Resize some fonts now to avoid costly, constant resizing. */
	[name_font setFontSize:20];
	[(SebFont *)[base_sebum getSebumByName:"fonts/digital2"] setFontSize:14];
    }

    /* Timers. */
    install_int_ex(ticker, BPS_TO_TIMER(GAME_SPEED));
    install_int_ex(fps_ticker, SECS_TO_TIMER(1));

    set_display_switch_callback(SWITCH_IN, switch_into_game);
}

static void game_loop_shutdown(void)
{
    remove_int(ticker);
    remove_int(fps_ticker);

    FREE_BITMAP(left_status);
    FREE_BITMAP(right_status);
    FREE_BITMAP(dbuf);
    FREE_BITMAP(real_dbuf);

#ifndef NO_VIDEO_BITMAPS
    active_page = NULL;

    /* If we use page flipping, make sure we are in the correct part
       of the screen. */
    if (raid_use_triple_buffering) {
	request_video_bitmap(page[0]);
    }
    else if (raid_use_page_flipping) {
	show_video_bitmap(page[0]);
    }
#endif
}

int game_init(void)
{
    if (input_init() < 0)
        return -1;

    name_font = (SebFont *)[base_sebum getSebumByName:MENU_FONT_NAME];
    main_font = name_font;

    if (not main_font || not name_font)
	return -1;

#ifndef NO_VIDEO_BITMAPS
    game_init_video_bitmaps();
#endif

    return 0;
}

void game_shutdown(void)
{
#ifndef NO_VIDEO_BITMAPS
    game_shutdown_video_bitmaps();
#endif

    input_shutdown();
}
