/*
 * Copyright (C) Volition, Inc. 1999.  All rights reserved.
 *
 * All source code herein is the property of Volition, Inc. You may not sell 
 * or otherwise commercially exploit the source or things you created based on the 
 * source.
 *
*/ 



#include <cctype>

#include "cfile/cfile.h"
#include "freespace.h"
#include "gamehelp/contexthelp.h"
#include "gamesequence/gamesequence.h"
#include "gamesnd/gamesnd.h"
#include "globalincs/alphacolors.h"
#include "graphics/font.h"
#include "io/key.h"
#include "menuui/mainhallmenu.h"
#include "menuui/readyroom.h"
#include "menuui/techmenu.h"	// for tech menu reset stuff
#include "mission/missionload.h"
#include "mission/missionparse.h"
#include "mission/missioncampaign.h"
#include "missionui/missionscreencommon.h"
#include "parse/parselo.h"
#include "pilotfile/pilotfile.h"
#include "playerman/managepilot.h"
#include "playerman/player.h"
#include "popup/popup.h"
#include "ui/ui.h"
#include "ui/uidefs.h"



#define MAX_MISSIONS	1024

int Mission_list_coords[GR_NUM_RESOLUTIONS][4] = {
	{ // GR_640
		33, 108, 402, 279
	},
	{ // GR_1024
		43, 175, 402, 459
	}
};

int Campaign_list_coords[GR_NUM_RESOLUTIONS][4] = {
	{ // GR_640
		491, 108, 115, 279
	},
	{ // GR_1024
		491, 175, 115, 459
	}
};


#define C_SUBTEXT_X	19
// x coordinate offsets for data when campaign tab active
#define C_TEXT_X		0

// x coordinate offsets for data when mission tab active
#define M_TEXT_X		C_SUBTEXT_X //0

#define MODE_CAMPAIGNS	0
#define MODE_MISSIONS	1

#define MAX_LINES					200		// NOLINT(modernize-macro-to-enum)
#define NUM_BUTTONS				11
#define LIST_BUTTONS_MAX		42

#define SCROLL_UP_BUTTON		0
#define SCROLL_DOWN_BUTTON		1
#define MISSION_TAB				2
#define CAMPAIGN_TAB				3
#define HELP_BUTTON				4
#define COMMIT_BUTTON			5
#define OPTIONS_BUTTON			6
#define TECH_DATABASE_BUTTON	7
#define SIMULATOR_BUTTON		8
#define CUTSCENES_BUTTON		9
#define CREDITS_BUTTON			10

#define X_COORD		0
#define Y_COORD		1
#define W_COORD		2
#define H_COORD		3

#define CAMPAIGN_MISSION_HASH_SIZE 307

struct sim_room_buttons {
	const char *filename;
	int x, y, xt, yt;
	int hotspot;
	UI_BUTTON button;  // because we have a class inside this struct, we need the constructor below..

	sim_room_buttons(const char *name, int x1, int y1, int xt1, int yt1, int h) : filename(name), x(x1), y(y1), xt(xt1), yt(yt1), hotspot(h) {}
};

static sim_room_buttons Buttons[GR_NUM_RESOLUTIONS][NUM_BUTTONS] = {
//XSTR:OFF
	{		// GR_640
		sim_room_buttons("LMB_04",		1,		99,	-1,	-1,	4),
		sim_room_buttons("LMB_05",		1,		381,	-1,	-1,	5),
		sim_room_buttons("LMB_06",		6,		438,	40,	445,	6),
		sim_room_buttons("LMB_07",		6,		457,	40,	462,	7),
		sim_room_buttons("LMB_08",		534,	426,	500,	440,	8),
		sim_room_buttons("LMB_09",		571,	426,	572,	413,	9),
		sim_room_buttons("LMB_10",		534,	455,	480,	462,	10),

		sim_room_buttons("TDB_00",		7,		3,		37,	7,		0),
		sim_room_buttons("TDB_01",		7,		18,	37,	23,	1),
		sim_room_buttons("TDB_02",		7,		34,	37,	38,	2),
		sim_room_buttons("TDB_03",		7,		49,	37,	54,	3),
	},
	{		// GR_1024
		sim_room_buttons("2_LMB_04",	2,		159,	-1,	-1,	4),
		sim_room_buttons("2_LMB_05",	2,		609,	-1,	-1,	5),
		sim_room_buttons("2_LMB_06",	10,	701,	64,	712,	6),
		sim_room_buttons("2_LMB_07",	10,	732,	64,	739,	7),
		sim_room_buttons("2_LMB_08",	854,	681,	800, 704,	8),
		sim_room_buttons("2_LMB_09",	914,	681,	915, 660,	9),
		sim_room_buttons("2_LMB_10",	854,	728,	780, 736,	10),

		sim_room_buttons("2_TDB_00",	12,	5,		59,	12,	0),
		sim_room_buttons("2_TDB_01",	12,	31,	59,	37,	1),
		sim_room_buttons("2_TDB_02",	12,	56,	59,	62,	2),
		sim_room_buttons("2_TDB_03",	12,	81,	59,	88,	3),
	}
//XSTR:ON
};

SCP_vector<sim_mission> Sim_Missions;
SCP_vector<sim_mission> Sim_CMissions;

const char* Sim_filename[GR_NUM_RESOLUTIONS] = {
	"LoadMission",
	"2_LoadMission"
};
const char* Sim_mask_filename[GR_NUM_RESOLUTIONS] = {
	"LoadMission-m",
	"2_LoadMission-m"
};

const char* Campaign_filename[GR_NUM_RESOLUTIONS] = {
	"Campaign",
	"2_Campaign"
};
const char* Campaign_mask_filename[GR_NUM_RESOLUTIONS] = {
	"Campaign-m",
	"2_Campaign-m"
};

const char* Sim_room_slider_filename[GR_NUM_RESOLUTIONS] = {
	"slider",
	"2_slider"
};

// misc text. ("Mission" and "Filename"
#define NUM_SIM_MISC_TEXT				2
#define SIM_MISC_TEXT_MISSION			0
#define SIM_MISC_TEXT_FILENAME		1
int Sim_misc_text_coords[GR_NUM_RESOLUTIONS][NUM_SIM_MISC_TEXT][2] = {
	{ // GR_640
		{33, 95},
		{491, 95}
	}, 
	{ // GR_1024
		{43, 155},
		{491, 155}
	}
};

int Sim_room_slider_coords[GR_NUM_RESOLUTIONS][4] = {
	{ // GR_640
		4, 131, 20, 245
	},
	{ // GR_1024
		5, 209, 32, 392
	}
};

// readyroom text line stuff
#define READYROOM_LINE_CAMPAIGN	1
#define READYROOM_LINE_CMISSION	2
#define READYROOM_LINE_MISSION	3

#define READYROOM_FLAG_FROM_VOLITION			(1<<0)			// volition made
static struct {	
	int type;					// see READYROOM_LINE_* defines above
	const char *name;
	const char *filename;
	int x;						// X coordinate of line
	int y;						// Y coordinate of line
	int flags;					// special flags, see READYROOM_FLAG_* defines above
} sim_room_lines[MAX_LINES];

static char Cur_campaign[MAX_FILENAME_LEN];
static char *Mission_filenames[MAX_MISSIONS] = { NULL };
static char *Standalone_mission_names[MAX_MISSIONS] = { NULL };
static int  Standalone_mission_flags[MAX_MISSIONS];
static char *Campaign_missions[MAX_MISSIONS] = { NULL };
static char *Campaign_mission_names[MAX_CAMPAIGN_MISSIONS] = { NULL };
static int Campaign_mission_flags[MAX_MISSIONS];
static int Simroom_show_all = 0;
static int Standalone_mission_names_inited = 0;
static int Campaign_mission_names_inited = 0;
static int Num_standalone_missions;
static int Num_campaign_missions;
//static int Num_player_missions;
static int Scroll_offset;
static int Selected_line;
static int Num_lines;
static int Num_campaign_missions_with_info = 0;
static int Num_standalone_missions_with_info = 0;
static int list_x1;
static int list_x2;
static int list_y;
static int list_w1;
static int list_w2;
static int list_h;
static int Background_bitmap;
static int Campaign_background_bitmap_mask;
static UI_WINDOW Ui_window;
static UI_BUTTON List_buttons[LIST_BUTTONS_MAX];  // buttons for each line of text in list

int Sim_room_overlay_id;
int Campaign_room_overlay_id;

// globals
bool Campaign_room_no_campaigns = false;

// slider stuff
static UI_SLIDER2 Sim_room_slider;

typedef struct hash_node {
	hash_node *next;
	const char *filename;
} hash_node;

static hash_node *Campaign_mission_hash_table[CAMPAIGN_MISSION_HASH_SIZE];
static int Hash_table_inited;

// special icons (1.04 + stuff)
#define NUM_MISSION_ICONS			1
#define MISSION_ICON_VOLITION		0				// mini volition death's head :)

// icon offsets (see LIST_ defines above
//#define MISSION_ICON_VOLITION_X				(46)
#define MISSION_ICON_VOLITION_Y_OFFSET		(-1)

// icon offsets
static int Sim_volition_icon_x[GR_NUM_RESOLUTIONS] = {
	38,
	49
};

// special icons themselves
int Mission_icon_bitmaps[NUM_MISSION_ICONS];
//XSTR:OFF
const char *Mission_icon_bitmap_filenames[NUM_MISSION_ICONS] = {
	"icon-volition"	
};
//XSTR:ON
void sim_room_load_mission_icons();
void sim_room_unload_mission_icons();
void sim_room_blit_icons(int line_index, int y_start, fs_builtin_mission *fb = NULL, int is_md = 0);

const std::shared_ptr<scripting::Hook<>> OnCampaignBeginHook = scripting::Hook<>::Factory(
	"On Campaign Begin", "Called when a campaign is started from the beginning or is reset",
	{
		{ "Campaign", "string", "The campaign filename (without the extension)" },
	});


// Finds a hash value for mission filename
//
// returns hash value
int hash_filename(const char *filename) {
	ulonglong hash_val = 0;
	const char *ptr = filename;
	
	// Don't hash .fsm extension, convert all to upper case
	for (int i=0; i < ((signed int)(strlen(filename)) - 4); i++) {
		hash_val = (hash_val << 4) + SCP_toupper(*ptr++);
	}

	return int(hash_val % CAMPAIGN_MISSION_HASH_SIZE);
}

// insert filename into Campaign_mission_hash_table
//
// returns 1 if successful, 0 if could not allocate memory
int hash_insert(const char *filename) {
	int hash_val = hash_filename(filename);
	hash_node *cur_node;

	// Check if table empty
	if (Campaign_mission_hash_table[hash_val] == NULL) {
		Campaign_mission_hash_table[hash_val] = new hash_node;

		cur_node = Campaign_mission_hash_table[hash_val];

		if (cur_node == NULL) {
			// Unable to allocate memory
			return 0;
		}
	} else {
		// Walk down list to first empty node
		cur_node = Campaign_mission_hash_table[hash_val];
		while (cur_node->next != NULL) {
			cur_node = cur_node->next;
		}

		// Create new node
		cur_node->next = new hash_node;

		if (cur_node->next == NULL) {
			// unable to allocate memory
			return 0;
		} else {
			cur_node = cur_node->next;
		}
	}

	// Initialize data
	cur_node->next = NULL;
	cur_node->filename = filename;

	// Return successs
	return 1;
}

// Checks if a filename already exitst in the hash table
//
// returns 1 if found (collision), 0 if no collision
int campaign_mission_hash_collision(const char *filename)
{
	int hash_val = hash_filename(filename);
	hash_node *cur_node = Campaign_mission_hash_table[hash_val];

	if (cur_node == NULL) {
		return 0;
	}

	do {
		if (!stricmp(filename, cur_node->filename)) {
			return 1;
		}

		cur_node = cur_node->next;
	} while (cur_node != NULL);

	// Ran out of stuff to check
	return 0;
}

// builds hash table of campaign mission filenames
//
// returns 1 if successful, 0 if not successful
int build_campaign_mission_filename_hash_table()
{
	int rval;
	// Go through all campaign missions
	for (int i=0; i<Num_campaign_missions; i++) {
		rval = hash_insert(Campaign_missions[i]);
		if (rval == 0) {
			return 0;
		}
	}

	// successful
	return 1;
}

// deletes hash table nodes
//
void campaign_mission_hash_table_delete()
{
	hash_node *cur_node;

	for (int i=0; i<CAMPAIGN_MISSION_HASH_SIZE; i++) {
		// Look for entries into array
		if (Campaign_mission_hash_table[i] != NULL) {
			cur_node = Campaign_mission_hash_table[i];

			// Walk down the list deleting self
			while (cur_node->next != NULL) {
				hash_node *temp = cur_node->next;
				delete cur_node;
				cur_node = temp;
			}

			// Delete last node
			delete cur_node;
			Campaign_mission_hash_table[i] = NULL;
		}
	}
}


// add a line of sim_room smuck to end of list
int sim_room_line_add(int type, const char *name, const char *filename, int x, int y, int flags)
{
	if (Num_lines >= MAX_LINES)
		return 0;

	sim_room_lines[Num_lines].type = type;
	sim_room_lines[Num_lines].name = name;
	sim_room_lines[Num_lines].filename = filename;
	sim_room_lines[Num_lines].x = x;
	sim_room_lines[Num_lines].y = y;
	sim_room_lines[Num_lines].flags = flags;
	return Num_lines++;
}

// build up a list of all missions in all campaigns.
int sim_room_campaign_mission_filter(const char *filename)
{
	int num;

	num = mission_campaign_get_mission_list(filename, &Campaign_missions[Num_campaign_missions], MAX_MISSIONS - Num_campaign_missions);
	if (num < 0)
		return 0;

	Num_campaigns++;
	Num_campaign_missions += num;
	return 1;
}

// filter out all missions already used in existing campaigns
int sim_room_standalone_mission_filter(const char *filename)
{
	int type;
	char mission_name[255];

	// Check if a campaign mission (single and multi)
	if (campaign_mission_hash_collision(filename)) {
		return 0;
	} 

	// Check if a standalone multi mission OR Mdisk mission with data
	type = mission_parse_is_multi(filename, mission_name);
	if (type && !(type & MISSION_TYPE_SINGLE))
		return 0;

	return 1;
}

// builds up list of standalone missions and adds them to missions simulator
// processes one mission per frame
//
// returns 1 if finished with all missions, 0 otherwise
//
int build_standalone_mission_list_do_frame(bool API_Access)
{
	int font_height = gr_get_font_height();
	SCP_string popup_str;
	bool lcl_weirdness = false;
	
	// When no standalone missions in data directory
	if (Num_standalone_missions == 0) {
		Standalone_mission_names_inited = 1;
		return 1;
	}

	// Set global variable so we we'll have list available next time
	Standalone_mission_names[Num_standalone_missions_with_info] = NULL;
	Standalone_mission_flags[Num_standalone_missions_with_info] = 0;

	if (Num_standalone_missions > 0) {  // sanity check
		if (strlen(Mission_filenames[Num_standalone_missions_with_info]) < MAX_FILENAME_LEN - 4) { // sanity check?
			// tack on an extension
			auto filename = cf_add_ext(Mission_filenames[Num_standalone_missions_with_info], FS_MISSION_FILE_EXT);

			// update popup
			sprintf(popup_str, XSTR("Single Mission\n\n%s", 989), filename);
			popup_change_text(popup_str.c_str());

			// activate tstrings check
			Lcl_unexpected_tstring_check = &lcl_weirdness;

			// check if we can list the mission, if loading basic info didn't return an error code, and if we didn't find an XSTR mismatch
			bool condition = !mission_is_ignored(filename) && !get_mission_info(filename) && !lcl_weirdness;

			// maybe log
			if (lcl_weirdness)
				mprintf(("Skipping %s due to XSTR mismatch\n", filename));

			// deactivate tstrings check
			Lcl_unexpected_tstring_check = nullptr;

			if (condition) {
				Standalone_mission_names[Num_standalone_missions_with_info] = vm_strdup(The_mission.name);
				Standalone_mission_flags[Num_standalone_missions_with_info] = The_mission.game_type;
				int y = Num_lines * (font_height + 2);

				//Add mission data to the API
				if (API_Access) {
					sim_mission api_mission;
					api_mission.name = The_mission.name;
					api_mission.filename = filename;
					api_mission.mission_desc = The_mission.mission_desc;
					api_mission.author = The_mission.author;
					api_mission.visible = 1;

					Sim_Missions.push_back(api_mission);
				}

				// determine some extra information
				int flags = 0;
				auto fb = game_find_builtin_mission(filename);
				if((fb != NULL) && (fb->flags & FSB_FROM_VOLITION)){
					flags |= READYROOM_FLAG_FROM_VOLITION;
				}

				// add the line
				sim_room_line_add(READYROOM_LINE_MISSION, Standalone_mission_names[Num_standalone_missions_with_info], Mission_filenames[Num_standalone_missions_with_info], list_x1 + M_TEXT_X, y, flags);			
			}
		}

		Num_standalone_missions_with_info++;
	}

	if (Num_standalone_missions_with_info == Num_standalone_missions) {
		Standalone_mission_names_inited = 1;
		return 1;
	} else {
		return 0;
	}
}

// builds up list of already played missions in a campaign and adds them to missions simulator
// processes one mission per frame
//
// returns 1 if finished with all missions, 0 otherwise
//
int build_campaign_mission_list_do_frame(bool API_Access)
{
	int font_height = gr_get_font_height();
	SCP_string popup_str;
	static int valid_missions_with_info = 0; // we use this to avoid blank entries in the mission list

	// When no campaign files in data directory
	if (Campaign.num_missions == 0) {
		Campaign_mission_names_inited = 1;
		return 1;
	}

	// change popup
	sprintf(popup_str, XSTR("Campaign Mission\n\n%s", 990), Campaign.missions[Num_campaign_missions_with_info].name);
	popup_change_text(popup_str.c_str());

	// Set global variable so we we'll have list available next time
	Campaign_mission_names[Num_campaign_missions_with_info] = NULL;
	Campaign_mission_flags[Num_campaign_missions_with_info] = 0;

	// Only allow missions already completed
	if (Campaign.missions[Num_campaign_missions_with_info].completed || (Simroom_show_all || API_Access)) 
	{
		if (!get_mission_info(Campaign.missions[Num_campaign_missions_with_info].name)) 
		{
			auto filename = Campaign.missions[Num_campaign_missions_with_info].name;
			
			// add to list
			Campaign_mission_names[Num_campaign_missions_with_info] = vm_strdup(The_mission.name);
			Campaign_mission_flags[Num_campaign_missions_with_info] = The_mission.game_type;
			int y = valid_missions_with_info * (font_height + 2);

			// Add mission data to the API
			if (API_Access) {
				sim_mission api_mission;
				api_mission.name = The_mission.name;
				api_mission.filename = filename;
				api_mission.mission_desc = The_mission.mission_desc;
				api_mission.author = The_mission.author;
				api_mission.visible = Campaign.missions[Num_campaign_missions_with_info].completed;

				Sim_CMissions.push_back(api_mission);
			}

			// determine some extra information
			int flags = 0;
			auto fb = game_find_builtin_mission(Campaign.missions[Num_campaign_missions_with_info].name);
			if((fb != NULL) && (fb->flags & FSB_FROM_VOLITION))
			{
				flags |= READYROOM_FLAG_FROM_VOLITION;
			}				
	
			sim_room_line_add(READYROOM_LINE_CMISSION, Campaign_mission_names[Num_campaign_missions_with_info], Campaign.missions[Num_campaign_missions_with_info].name, list_x1 + C_SUBTEXT_X, y, flags);
			valid_missions_with_info++;
		}
	}

	Num_campaign_missions_with_info++;

	if (Num_campaign_missions_with_info == Campaign.num_missions) {
		valid_missions_with_info = 0;
		Campaign_mission_names_inited = 1;
		return 1;
	} else {
		return 0;
	}
}

// Builds list of either (1) standalone missions or (2) already played missions in current campaign
// First time through, it builds list, next time it uses precompiled list
void sim_room_build_listing()
{
	int i, y, max_num_entries_viewable;
	int font_height = gr_get_font_height();

	Num_lines = y = 0;
	list_y = Mission_list_coords[gr_screen.res][1];
	list_h = Mission_list_coords[gr_screen.res][3];

	// Stand alone single player missions.
	if (Player->readyroom_listing_mode == MODE_MISSIONS) {
		if (Hash_table_inited) {
			if (!Standalone_mission_names_inited) {  // Is this the first time through
				// build_list_do_frame builds list and adds sim room line and sets Standalone_mission_names_inited
				popup_till_condition([]() -> int { return build_standalone_mission_list_do_frame(false); },
					POPUP_CANCEL,
					XSTR("Loading missions", 991));
			} else {
				for (i=0; i<Num_standalone_missions_with_info; i++) {
					if (Standalone_mission_names[i]) {
						// determine some extra information
						int flags = 0;
						auto full_filename = cf_add_ext(Mission_filenames[i], FS_MISSION_FILE_EXT);
						auto fb = game_find_builtin_mission(full_filename);
						if((fb != NULL) && (fb->flags & FSB_FROM_VOLITION)){
							flags |= READYROOM_FLAG_FROM_VOLITION;
						}
						
						sim_room_line_add(READYROOM_LINE_MISSION, Standalone_mission_names[i], Mission_filenames[i], list_x1 + M_TEXT_X, y, flags);
						y += font_height + 2;
					}
				}
			}
		}
	} else {
		// Campaign missions
		list_y += font_height + 2;
		list_h -= font_height - 2;

		if (!Campaign_mission_names_inited) {  // Is this the first time through
			// builds list, adds sim room line and sets Campaign_mission_names_inited
			popup_till_condition([]() -> int { return build_campaign_mission_list_do_frame(false); },
				POPUP_CANCEL,
				XSTR("Loading campaign missions", 992));
		} else {
			for (i=0; i<Num_campaign_missions_with_info; i++) {
				if (Campaign_mission_names[i]) {
					// determine some extra information
					int flags = 0;
					auto full_filename = cf_add_ext(Campaign.missions[i].name, FS_MISSION_FILE_EXT);
					auto fb = game_find_builtin_mission(full_filename);
					if((fb != NULL) && (fb->flags & FSB_FROM_VOLITION)){
						flags |= READYROOM_FLAG_FROM_VOLITION;
					}					

					sim_room_line_add(READYROOM_LINE_CMISSION, Campaign_mission_names[i], Campaign.missions[i].name, list_x1 + C_SUBTEXT_X, y, flags);
					y += font_height + 2;	// Goober5000 - added +2 to conform with above
				}
			}
		}
	}

	// free up memory from parsing all of those missions
	stop_parse();

	// set appropriate slider size
	max_num_entries_viewable = list_h / (font_height + 2);
	Sim_room_slider.set_numberItems((Num_lines > max_num_entries_viewable) ? (Num_lines - max_num_entries_viewable) : 0);
}

void sim_room_reset_campaign_listing()
{
	// only reset if we got fully inited in the first place
	if (!Campaign_mission_names_inited)
		return;


	for (int i=0; i<Campaign.num_missions; i++) {
		if (Campaign_mission_names[i]) {
			vm_free(Campaign_mission_names[i]);
			Campaign_mission_names[i] = NULL;
		}
	}

	Campaign_mission_names_inited = 0;
	Num_campaign_missions_with_info = 0;

	sim_room_build_listing();
}

int sim_room_line_query_visible(int n)
{
	int y;

	if ((n < 0) || (n >= Num_lines))
		return 0;
	
	y = sim_room_lines[n].y - sim_room_lines[Scroll_offset].y;
	if ((y < 0) || (y + gr_get_font_height() > list_h))
		return 0;

	return 1;
}

void sim_room_scroll_screen_up()
{
	if (Scroll_offset > 0) {
		Scroll_offset--;

		if (Player->readyroom_listing_mode == MODE_MISSIONS)
		{
			Assert(Selected_line > Scroll_offset);
			while (!sim_room_line_query_visible(Selected_line))
			{
				Selected_line--;
			}
		}

		gamesnd_play_iface(InterfaceSounds::SCROLL);

	} else
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}

void sim_room_scroll_line_up()
{
	if (Selected_line) {
		Selected_line--;
		if (Selected_line < Scroll_offset)
		{
			Scroll_offset = Selected_line;
			Sim_room_slider.forceUp();
		}

		gamesnd_play_iface(InterfaceSounds::SCROLL);

	} else
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}

void sim_room_scroll_screen_down()
{
	if (sim_room_lines[Num_lines - 1].y + gr_get_font_height() > sim_room_lines[Scroll_offset].y + list_h) {
		Scroll_offset++;

		if (Player->readyroom_listing_mode == MODE_MISSIONS)
		{			
			while (!sim_room_line_query_visible(Selected_line))
			{
				Selected_line++;
				Assert(Selected_line < Num_lines);
			}
		}

		gamesnd_play_iface(InterfaceSounds::SCROLL);

	} else
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}

void sim_room_scroll_line_down()
{
	if (Selected_line < Num_lines - 1) {
		Selected_line++;

		Assert(Selected_line > Scroll_offset);
		while (!sim_room_line_query_visible(Selected_line))
		{
			Scroll_offset++;
			Sim_room_slider.forceDown();
		}

		gamesnd_play_iface(InterfaceSounds::SCROLL);

	} else
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}

/*  Goober5000 - why are there two nearly identical functions?
	(campaign_room_reset_campaign and sim_room_reset_campaign)
	Looks like this function should be deprecated...

// returns: 0 = success, !0 = aborted or failed
int sim_room_reset_campaign()
{
	int z, rval = 1;

	z = popup(PF_TITLE_BIG | PF_TITLE_RED, 2, POPUP_CANCEL, POPUP_OK, XSTR( "Warning\nThis will cause all progress in your\ncurrent campaign to be lost", 110) );

	if (z) {
		mission_campaign_savefile_delete(Campaign.filename);
		mission_campaign_load(Campaign.filename);

		rval = 0;
	}

	return rval;
}
*/

// Decide if we should offer choice to resume this savegame
int sim_room_can_resume_savegame(char * /*savegame_filename*/)
{
	#ifdef FREESPACE_SAVERESTORE_SYSTEM
	char savegame_mission[MAX_FILENAME_LEN];

	if (state_read_description(savegame_filename, NULL, savegame_mission)) {
		return 0;
	}

	if (stricmp(Game_current_mission_filename, savegame_mission)) {
		return 0;
	}

	return 1;
	#else
	return 0;
	#endif
}

int readyroom_continue_campaign()
{
	// check for possible mission loop, need to do this before calling mission_campaign_next_mission()!
	if ( !(Game_mode & GM_MULTIPLAYER) && !Campaign.loop_enabled && (Campaign.current_mission == -1) &&
		(Campaign.prev_mission != -1) && (Campaign.next_mission != -1) )
	{
		if (Campaign.missions[Campaign.prev_mission].flags & CMISSION_FLAG_HAS_LOOP) {
			// NOTE: the order of these calls is *very* important

			// we must manually set the current mission to the *previous* mission and the mission name.
			// this isn't the same thing that is done in mission_campaign_next_mission(), but it's
			// cleaner to do this here than it is to hack that function to do the same thing
			Campaign.current_mission = Campaign.prev_mission;
			strcpy_s( Game_current_mission_filename, Campaign.missions[Campaign.current_mission].name );

			// set the bit for campaign mode
			Game_mode |= GM_CAMPAIGN_MODE;

			// eval the mission (may be needed to setup loop mission stuff)
			if (Campaign.loop_mission == CAMPAIGN_LOOP_MISSION_UNINITIALIZED)
				mission_campaign_eval_next_mission();
	
			// only proceed to loop if we have a loop
			if (Campaign.loop_mission > 0) {
				gameseq_post_event(GS_EVENT_LOOP_BRIEF);
				return 0;
			}
		}
	}

	int mc_rval = mission_campaign_next_mission();
	if (mc_rval == -1)
	{  // is campaign and next mission valid?
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
		popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, XSTR( "The campaign is over.  To replay the campaign, either create a new pilot or restart the campaign in the campaign room.", 112) );
		return -1;
	}
	else if(mc_rval == -2)
	{
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
		popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, NOX("The current campaign has no missions") );
		return -1;
	}

	// set the bit for campaign mode
	Game_mode |= GM_CAMPAIGN_MODE;
	gameseq_post_event( GS_EVENT_START_GAME );	

	return 0;
}

void sim_room_commit()
{
	if ((Selected_line >= Num_lines) || !sim_room_lines[Selected_line].filename) {
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
		return;
	}

	strcpy_s(Game_current_mission_filename, sim_room_lines[Selected_line].filename);

	Game_mode &= ~(GM_CAMPAIGN_MODE);						// be sure this bit is clear

	gameseq_post_event(GS_EVENT_START_GAME);
	gamesnd_play_iface(InterfaceSounds::COMMIT_PRESSED);
}

int sim_room_button_pressed(int n)
{
	switch (n) {
		case SCROLL_UP_BUTTON:
			sim_room_scroll_screen_up();
			Sim_room_slider.forceUp();
			break;

		case SCROLL_DOWN_BUTTON:
			sim_room_scroll_screen_down();
			Sim_room_slider.forceDown();
			break;

		case MISSION_TAB:
			Simroom_show_all = 0;
			Player->readyroom_listing_mode = MODE_MISSIONS;
			Selected_line = Scroll_offset = 0;
			gamesnd_play_iface(InterfaceSounds::USER_SELECT);
			sim_room_build_listing();
			break;

		case CAMPAIGN_TAB:
			if ( !strlen(Campaign.filename) ) {
				if (Campaign_load_failure == CAMPAIGN_ERROR_MISSING) {
					popup(PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, XSTR("The currently active campaign cannot be found.  Unable to switch to campaign mode.", 1612));
				} else {
					popup(PF_USE_AFFIRMATIVE_ICON | PF_NO_NETWORKING, 1, POPUP_OK, XSTR("The currently active campaign cannot be loaded.  Unable to switch to campaign mode.", 1816));
				}
				break;
			}

			if (Player->readyroom_listing_mode != MODE_CAMPAIGNS)
				Simroom_show_all = 0;

			Player->readyroom_listing_mode = MODE_CAMPAIGNS;
			Scroll_offset = 0;
			gamesnd_play_iface(InterfaceSounds::USER_SELECT);
			sim_room_build_listing();
			break;

		case COMMIT_BUTTON:
			sim_room_commit();
			break;

		case HELP_BUTTON:
			launch_context_help();
			gamesnd_play_iface(InterfaceSounds::HELP_PRESSED);
			break;

		case OPTIONS_BUTTON:
			gamesnd_play_iface(InterfaceSounds::SWITCH_SCREENS);
			gameseq_post_event(GS_EVENT_OPTIONS_MENU);
			return 1;

		case TECH_DATABASE_BUTTON:
			gamesnd_play_iface(InterfaceSounds::SWITCH_SCREENS);
			gameseq_post_event(GS_EVENT_TECH_MENU);
			return 1;

		case CUTSCENES_BUTTON:
			gamesnd_play_iface(InterfaceSounds::SWITCH_SCREENS);
			gameseq_post_event(GS_EVENT_GOTO_VIEW_CUTSCENES_SCREEN);
			return 1;

		case CREDITS_BUTTON:
			gamesnd_play_iface(InterfaceSounds::SWITCH_SCREENS);
			gameseq_post_event(GS_EVENT_CREDITS);
			return 1;
	}

	return 0;
}

// do nothing
void sim_room_scroll_capture()
{
}

// ---------------------------------------------------------------------
// mission_sim_room_init()
//
// Initialize the sim_room assignment screen system.  Called when GS_STATE_sim_room_SCREEN
// is entered.
//

void sim_room_init()
{
	int i;
	sim_room_buttons *b;
	char wild_card[8];

	list_x1 = Mission_list_coords[gr_screen.res][0];
	list_x2 = Campaign_list_coords[gr_screen.res][0];
	list_y = Mission_list_coords[gr_screen.res][1];
	list_w1 = Mission_list_coords[gr_screen.res][2];
	list_w2 = Campaign_list_coords[gr_screen.res][2];
	list_h = Mission_list_coords[gr_screen.res][3];

	// force mode to valid range.  I once had a bogus value here.  Don't know how that happened, though.
	if (Player->readyroom_listing_mode != MODE_MISSIONS)
		Player->readyroom_listing_mode = MODE_CAMPAIGNS;

	*Game_current_mission_filename = 0;
	common_set_interface_palette("InterfacePalette");  // set the interface palette
	Ui_window.create(0, 0, gr_screen.max_w_unscaled, gr_screen.max_h_unscaled, 0);
	Ui_window.set_mask_bmap(Sim_mask_filename[gr_screen.res]);

	for (i=0; i<NUM_BUTTONS; i++) {
		b = &Buttons[gr_screen.res][i];

		b->button.create(&Ui_window, "", b->x, b->y, 60, 30, (i < 2), 1);
		// set up callback for when a mouse first goes over a button
		b->button.set_highlight_action(common_play_highlight_sound);
		b->button.set_bmaps(b->filename);
		b->button.link_hotspot(b->hotspot);
	}

	// screen/button specific text
	Ui_window.add_XSTR("Single Missions", 1060, Buttons[gr_screen.res][MISSION_TAB].xt, Buttons[gr_screen.res][MISSION_TAB].yt, &Buttons[gr_screen.res][MISSION_TAB].button, UI_XSTR_COLOR_GREEN);
	Ui_window.add_XSTR("Campaign Missions", 1061, Buttons[gr_screen.res][CAMPAIGN_TAB].xt, Buttons[gr_screen.res][CAMPAIGN_TAB].yt, &Buttons[gr_screen.res][CAMPAIGN_TAB].button, UI_XSTR_COLOR_GREEN);
	Ui_window.add_XSTR("Help", 928, Buttons[gr_screen.res][HELP_BUTTON].xt, Buttons[gr_screen.res][HELP_BUTTON].yt, &Buttons[gr_screen.res][HELP_BUTTON].button, UI_XSTR_COLOR_GREEN);
	Ui_window.add_XSTR("Commit", 1062, Buttons[gr_screen.res][COMMIT_BUTTON].xt, Buttons[gr_screen.res][COMMIT_BUTTON].yt, &Buttons[gr_screen.res][COMMIT_BUTTON].button, UI_XSTR_COLOR_PINK);
	Ui_window.add_XSTR("Options", 1036, Buttons[gr_screen.res][OPTIONS_BUTTON].xt, Buttons[gr_screen.res][OPTIONS_BUTTON].yt, &Buttons[gr_screen.res][OPTIONS_BUTTON].button, UI_XSTR_COLOR_GREEN);
	
	// common tab button text
	Ui_window.add_XSTR("Technical Database", 1055, Buttons[gr_screen.res][TECH_DATABASE_BUTTON].xt,  Buttons[gr_screen.res][TECH_DATABASE_BUTTON].yt, &Buttons[gr_screen.res][TECH_DATABASE_BUTTON].button, UI_XSTR_COLOR_GREEN);
	Ui_window.add_XSTR("Mission Simulator", 1056, Buttons[gr_screen.res][SIMULATOR_BUTTON].xt,  Buttons[gr_screen.res][SIMULATOR_BUTTON].yt, &Buttons[gr_screen.res][SIMULATOR_BUTTON].button, UI_XSTR_COLOR_GREEN);
	Ui_window.add_XSTR("Cutscenes", 1057, Buttons[gr_screen.res][CUTSCENES_BUTTON].xt,  Buttons[gr_screen.res][CUTSCENES_BUTTON].yt, &Buttons[gr_screen.res][CUTSCENES_BUTTON].button, UI_XSTR_COLOR_GREEN);
	Ui_window.add_XSTR("Credits", 1058, Buttons[gr_screen.res][CREDITS_BUTTON].xt,  Buttons[gr_screen.res][CREDITS_BUTTON].yt, &Buttons[gr_screen.res][CREDITS_BUTTON].button, UI_XSTR_COLOR_GREEN);

	// misc text - not associated with any buttons
	Ui_window.add_XSTR("Mission", 1063, Sim_misc_text_coords[gr_screen.res][SIM_MISC_TEXT_MISSION][0], Sim_misc_text_coords[gr_screen.res][SIM_MISC_TEXT_MISSION][1], NULL, UI_XSTR_COLOR_GREEN);
	Ui_window.add_XSTR("Filename", 1064, Sim_misc_text_coords[gr_screen.res][SIM_MISC_TEXT_FILENAME][0], Sim_misc_text_coords[gr_screen.res][SIM_MISC_TEXT_FILENAME][1], NULL, UI_XSTR_COLOR_GREEN);

	for (i=0; i<LIST_BUTTONS_MAX; i++) {
		List_buttons[i].create(&Ui_window, "", 0, 0, 60, 30, 0, 1);
		List_buttons[i].hide();
		List_buttons[i].disable();
	}

	// set up sim_rooms for buttons so we draw the correct animation frame when a key is pressed
	Buttons[gr_screen.res][SCROLL_UP_BUTTON].button.set_hotkey(KEY_PAGEUP);
	Buttons[gr_screen.res][SCROLL_DOWN_BUTTON].button.set_hotkey(KEY_PAGEDOWN);
	Buttons[gr_screen.res][COMMIT_BUTTON].button.set_hotkey(KEY_CTRLED | KEY_ENTER);	

	Background_bitmap = bm_load(Sim_filename[gr_screen.res]);

	// load in help overlay bitmap	
	Sim_room_overlay_id = help_overlay_get_index(SIM_ROOM_OVERLAY);
	help_overlay_set_state(Sim_room_overlay_id,gr_screen.res,0);

	Scroll_offset = Selected_line = 0;

	strcpy_s(Cur_campaign, Player->current_campaign);
	if ( !mission_load_up_campaign() ) {
		mission_campaign_next_mission();
	} else {
		Campaign.filename[0] = 0;
		Campaign.num_missions = 0;

		// don't display the popup in the sim room - first because there is already logic to prevent listing campaign missions,
		// second because there's not much the player can do about it in the sim room, and third because displaying the popup
		// clears the error code
	}

	Num_campaign_missions = 0;
	Get_file_list_filter = sim_room_campaign_mission_filter;

	mission_campaign_build_list(false, false);	// no descs, no sorting

	Hash_table_inited = 0;
	if (build_campaign_mission_filename_hash_table()) {
		Hash_table_inited = 1;
	}

	// if there is no campaign available then force the use of mode_missions only
	if ( !strlen(Campaign.filename) )
		Player->readyroom_listing_mode = MODE_MISSIONS;

	// HACK
	GR_MAYBE_CLEAR_RES(Background_bitmap);
	if(Background_bitmap != -1){
		gr_set_bitmap(Background_bitmap);
		gr_bitmap(0, 0, GR_RESIZE_MENU);
	}
	Ui_window.draw();
	gr_flip();		

	Get_file_list_filter = sim_room_standalone_mission_filter;
	strcpy_s(wild_card, NOX("*"));
	strcat_s(wild_card, FS_MISSION_FILE_EXT);

#ifndef NDEBUG
	// Activate the check while we fill the mission list so that we don't potentially end up with dozens of string length warnings.
	// We don't actually remove mismatched missions from the list here; that will happen in build_standalone_mission_list_do_frame().
	bool dummy_buffer;
	Lcl_unexpected_tstring_check = &dummy_buffer;
#endif
	Num_standalone_missions = cf_get_file_list(MAX_MISSIONS, Mission_filenames, CF_TYPE_MISSIONS, wild_card, CF_SORT_NAME);
#ifndef NDEBUG
	Lcl_unexpected_tstring_check = nullptr;
#endif

	// set up slider with 0 items to start
	Sim_room_slider.create(&Ui_window, Sim_room_slider_coords[gr_screen.res][X_COORD], Sim_room_slider_coords[gr_screen.res][Y_COORD], Sim_room_slider_coords[gr_screen.res][W_COORD], Sim_room_slider_coords[gr_screen.res][H_COORD], 0, Sim_room_slider_filename[gr_screen.res], &sim_room_scroll_screen_up, &sim_room_scroll_screen_down, &sim_room_scroll_capture);

	Num_campaign_missions_with_info = Num_standalone_missions_with_info = Standalone_mission_names_inited = Campaign_mission_names_inited = 0;
	Simroom_show_all = 0;
	sim_room_build_listing();

	// load special mission icons
	sim_room_load_mission_icons();
}

// ---------------------------------------------------------------------
// sim_room_close()
//
// Cleanup the sim_room assignment screen system.  Called when GS_STATE_sim_room_SCREEN
// is left.
//
void sim_room_close()
{
	int i;

	for (i=0; i<Num_campaign_missions; i++) {
		if (Campaign_missions[i]) {
			vm_free(Campaign_missions[i]);
			Campaign_missions[i] = NULL;
		}
	}

	if (Background_bitmap >= 0)
		bm_release(Background_bitmap);

	if (Standalone_mission_names_inited){
		for (i=0; i<Num_standalone_missions; i++){
			if (Standalone_mission_names[i] != NULL){
				vm_free(Standalone_mission_names[i]);
				Standalone_mission_names[i] = NULL;
			}
			Standalone_mission_flags[i] = 0;
		}
	}

	if (Campaign_mission_names_inited) {
		for (i=0; i<Campaign.num_missions; i++) {
			if (Campaign_mission_names[i]) {
				vm_free(Campaign_mission_names[i]);
				Campaign_mission_names[i] = NULL;
			}
		}
	}

	for (i=0; i<Num_standalone_missions; i++) {
		vm_free(Mission_filenames[i]);
		Mission_filenames[i] = NULL;
	}

	// free global Campaign_* list stuff
	mission_campaign_free_list();

	campaign_mission_hash_table_delete();

	Ui_window.destroy();
	common_free_interface_palette();		// restore game palette
	Pilot.save_player();

	// unload special mission icons
	sim_room_unload_mission_icons();
}

//The mission list for the tech room is built each time the game state is entered
//so this method provides a way to do the exact same thing from scripting.
//It has some duplication of code but seemed the easiest way to provide similar
//functionality without a complete refactor.
void api_sim_room_build_mission_list(bool API_Access)
{
	char wild_card[8];

	Num_campaign_missions = 0;
	Get_file_list_filter = sim_room_campaign_mission_filter;

	mission_campaign_build_list(false, false);

	Hash_table_inited = 0;
	if (build_campaign_mission_filename_hash_table()) {
		Hash_table_inited = 1;
	}

	Get_file_list_filter = sim_room_standalone_mission_filter;
	strcpy_s(wild_card, NOX("*"));
	strcat_s(wild_card, FS_MISSION_FILE_EXT);

	Num_standalone_missions =
		cf_get_file_list(MAX_MISSIONS, Mission_filenames, CF_TYPE_MISSIONS, wild_card, CF_SORT_NAME);

	while (!build_standalone_mission_list_do_frame(API_Access)) {
	}
	while (!build_campaign_mission_list_do_frame(API_Access)) {
	}

	Num_campaign_missions_with_info = Num_standalone_missions_with_info = Standalone_mission_names_inited =
		Campaign_mission_names_inited = 0;

	int i;
	for (i = 0; i < Num_campaign_missions; i++) {
		if (Campaign_missions[i]) {
			vm_free(Campaign_missions[i]);
			Campaign_missions[i] = NULL;
		}
	}

	if (Standalone_mission_names_inited) {
		for (i = 0; i < Num_standalone_missions; i++) {
			if (Standalone_mission_names[i] != NULL) {
				vm_free(Standalone_mission_names[i]);
				Standalone_mission_names[i] = NULL;
			}
			Standalone_mission_flags[i] = 0;
		}
	}

	if (Campaign_mission_names_inited) {
		for (i = 0; i < Campaign.num_missions; i++) {
			if (Campaign_mission_names[i]) {
				vm_free(Campaign_mission_names[i]);
				Campaign_mission_names[i] = NULL;
			}
		}
	}

	for (i = 0; i < Num_standalone_missions; i++) {
		vm_free(Mission_filenames[i]);
		Mission_filenames[i] = NULL;
	}

	// free global Campaign_* list stuff
	mission_campaign_free_list();

	campaign_mission_hash_table_delete();
}

// ---------------------------------------------------------------------
// sim_room_do_frame()
//
// Called once per frame to process user input for the sim_room Assignment Screen
//
void sim_room_do_frame(float  /*frametime*/)
{
	char buf[CF_MAX_PATHNAME_LENGTH + 4]; // must hold at most the Campaign.name and a file extension (4 chars)
	int i, k, y, line;
	int font_height = gr_get_font_height();
	int select_tease_line = -1;  // line mouse is down on, but won't be selected until button released	

	if ( help_overlay_active(Sim_room_overlay_id) ) {
		Buttons[gr_screen.res][HELP_BUTTON].button.reset_status();
		Ui_window.set_ignore_gadgets(1);
	}

	k = Ui_window.process() & ~KEY_DEBUGGED;

	if ( (k > 0) || B1_JUST_RELEASED ) {
		if ( help_overlay_active(Sim_room_overlay_id) ) {
			help_overlay_set_state(Sim_room_overlay_id, gr_screen.res, 0);
			Ui_window.set_ignore_gadgets(0);
			k = 0;
		}
	}

	if ( !help_overlay_active(Sim_room_overlay_id) ) {
		Ui_window.set_ignore_gadgets(0);
	}

	switch (k) {
		case KEY_DOWN:  // scroll list down
			sim_room_scroll_line_down();
			break;

		case KEY_UP:  // scroll list up
			sim_room_scroll_line_up();
			break;

		case KEY_ESC:
			gameseq_post_event(GS_EVENT_MAIN_MENU);
			break;

		case KEY_CTRLED | KEY_UP:
			sim_room_button_pressed(TECH_DATABASE_BUTTON);
			break;

		case KEY_CTRLED | KEY_DOWN:
			sim_room_button_pressed(CUTSCENES_BUTTON);
			break;

		case KEY_TAB:
			if (Player->readyroom_listing_mode == MODE_CAMPAIGNS)
				Player->readyroom_listing_mode = MODE_MISSIONS;
			else if ( strlen(Campaign.filename) )
				Player->readyroom_listing_mode = MODE_CAMPAIGNS;
			else
				Player->readyroom_listing_mode = MODE_MISSIONS;

			Selected_line = Scroll_offset = Simroom_show_all = 0;
			gamesnd_play_iface(InterfaceSounds::USER_SELECT);
			sim_room_build_listing();
			break;

		case KEY_F2:
			gamesnd_play_iface(InterfaceSounds::SWITCH_SCREENS);
			gameseq_post_event(GS_EVENT_OPTIONS_MENU);
			break;

		case KEY_CTRLED | KEY_SHIFTED | KEY_S:
			if (Player->readyroom_listing_mode == MODE_CAMPAIGNS) {
				Simroom_show_all = !Simroom_show_all;
				sim_room_reset_campaign_listing();
			}
			break;
	}	// end switch

	for (i=0; i<NUM_BUTTONS; i++){
		if (Buttons[gr_screen.res][i].button.pressed()){
			if (sim_room_button_pressed(i)){
				return;
			}
		}
	}

	for (i=0; i<LIST_BUTTONS_MAX; i++) {
		if (List_buttons[i].is_mouse_on())
			select_tease_line = i + Scroll_offset;
	
		if (List_buttons[i].pressed()) {
			Selected_line = i + Scroll_offset;
			gamesnd_play_iface(InterfaceSounds::USER_SELECT);
		}
	}

	GR_MAYBE_CLEAR_RES(Background_bitmap);
	if (Background_bitmap >= 0) {
		gr_set_bitmap(Background_bitmap);
		gr_bitmap(0, 0, GR_RESIZE_MENU);
	}

	Ui_window.draw();

	for (i=TECH_DATABASE_BUTTON; i<=CREDITS_BUTTON; i++){
		if (Buttons[gr_screen.res][i].button.button_down()){
			break;
		}
	}

	if (i > CREDITS_BUTTON){
		Buttons[gr_screen.res][SIMULATOR_BUTTON].button.draw_forced(2);
	}

	if (!Buttons[gr_screen.res][CAMPAIGN_TAB].button.button_down() && !Buttons[gr_screen.res][MISSION_TAB].button.button_down()) {
		if (Player->readyroom_listing_mode == MODE_CAMPAIGNS){
			Buttons[gr_screen.res][CAMPAIGN_TAB].button.draw_forced(2);
		} else if (Player->readyroom_listing_mode == MODE_MISSIONS){
			Buttons[gr_screen.res][MISSION_TAB].button.draw_forced(2);
		}
	}

	font::set_font(font::FONT1);
	if (Player->readyroom_listing_mode == MODE_CAMPAIGNS) {
		gr_set_color_fast(&Color_text_heading);
		strcpy_s(buf, Campaign.name);
		font::force_fit_string(buf, 255, list_w1);
		gr_printf_menu(list_x1, Mission_list_coords[gr_screen.res][1], "%s", buf);

		if (Campaign.filename[0] != '\0') {			
			sprintf(buf, NOX("%s%s"), Campaign.filename, FS_CAMPAIGN_FILE_EXT);
			font::force_fit_string(buf, 255, list_w2);
			gr_printf_menu(list_x2, Mission_list_coords[gr_screen.res][1], "%s", buf);

			// blit the proper icons if necessary
			auto full_name = cf_add_ext(Campaign.filename,FS_CAMPAIGN_FILE_EXT);
			auto fb = game_find_builtin_mission(full_name);
			if(fb != NULL){
				// sim_room_blit_icons(0, Mission_list_coords[gr_screen.res][1], fb, 0);
			}
		}
	}

	line = Scroll_offset;
	while (sim_room_line_query_visible(line) && (line - Scroll_offset < LIST_BUTTONS_MAX)) {
		y = list_y + sim_room_lines[line].y - sim_room_lines[Scroll_offset].y;

		if (sim_room_lines[line].type != READYROOM_LINE_CAMPAIGN) {
			List_buttons[line - Scroll_offset].update_dimensions(list_x1, y, list_x2 + list_w2 - list_x1, font_height);
			List_buttons[line - Scroll_offset].enable();

		} else
			List_buttons[line - Scroll_offset].disable();

		if (line == Selected_line)
			gr_set_color_fast(&Color_text_selected);
		else if (line == select_tease_line)
			gr_set_color_fast(&Color_text_subselected);
		else
			gr_set_color_fast(&Color_text_normal);

		strcpy_s(buf, sim_room_lines[line].name);
		font::force_fit_string(buf, 255, list_x1 + list_w1 - sim_room_lines[line].x);
		gr_printf_menu(sim_room_lines[line].x, y, "%s", buf);

		if (sim_room_lines[line].filename) {
			strcpy_s(buf, sim_room_lines[line].filename);
			font::force_fit_string(buf, 255, list_w2);
			gr_printf_menu(list_x2, y, "%s", buf);
		}

		// blit additional icon information
		sim_room_blit_icons(line, y);

		line++;
	}

	i = line - Scroll_offset;
	while (i < LIST_BUTTONS_MAX)
		List_buttons[i++].disable();

	// blit help overlay if active
	help_overlay_maybe_blit(Sim_room_overlay_id, gr_screen.res);

	gr_flip();
}

void sim_room_blit_icons(int line_index, int y_start, fs_builtin_mission *fb, int  /*is_md*/)
{
	int is_from_volition = 0;	

	// determine icon status
	if(fb == NULL){
		is_from_volition = (sim_room_lines[line_index].flags & READYROOM_FLAG_FROM_VOLITION) ? 1 : 0;		
	} else {
		is_from_volition = (fb->flags & FSB_FROM_VOLITION) ? 1 : 0;		
	}

	// if the line is flagged as a volition file
	if(is_from_volition && (Mission_icon_bitmaps[MISSION_ICON_VOLITION] >= 0)){		
		gr_set_bitmap(Mission_icon_bitmaps[MISSION_ICON_VOLITION]);
		gr_bitmap(Sim_volition_icon_x[gr_screen.res], y_start + MISSION_ICON_VOLITION_Y_OFFSET, GR_RESIZE_MENU);
	}	
}

///  Campaign room stuff below
int Cr_list_coords[GR_NUM_RESOLUTIONS][4] = {
	{ // GR_640
		47, 21, 565, 233
	},
	{ // GR_1024
		64, 34, 916, 459
	}
};

int Cr_info_coords[GR_NUM_RESOLUTIONS][4] = {
	{ // GR_640
		28, 267, 476, 103
	},
	{ // GR_1024
		45, 427, 761, 165
	},
};

#define CR_NUM_BUTTONS					6

#define CR_SCROLL_UP_BUTTON			0
#define CR_SCROLL_DOWN_BUTTON			1
#define CR_SCROLL_INFO_UP_BUTTON		2
#define CR_SCROLL_INFO_DOWN_BUTTON	3
#define CR_RESET_BUTTON					4
#define CR_COMMIT_BUTTON				5

#define MAX_INFO_LINES		20

#define MAX_INFO_LINE_LEN	256

ui_button_info Cr_buttons[GR_NUM_RESOLUTIONS][CR_NUM_BUTTONS] = {
	{ // GR_640
		ui_button_info("CAB_00",	2,		42,	-1,	-1,	0),
		ui_button_info("CAB_01",	2,		89,	-1,	-1,	1),
		ui_button_info("CAB_02",	2,		279,	-1,	-1,	2),
		ui_button_info("CAB_03",	2,		325,	-1,	-1,	3),
		ui_button_info("CAB_04",	579,	353,	-1,	-1,	4),
		ui_button_info("CAB_05",	575,	434,	-1,	-1,	5),
	},
	{ // GR_1024
		ui_button_info("2_CAB_00",	3,		68,	-1,	-1,	0),
		ui_button_info("2_CAB_01",	3,		142,	-1,	-1,	1),
		ui_button_info("2_CAB_02",	3,		446,	-1,	-1,	2),
		ui_button_info("2_CAB_03",	3,		520,	-1,	-1,	3),
		ui_button_info("2_CAB_04",	927,	565,	-1,	-1,	4),
		ui_button_info("2_CAB_05",	920,	694,	-1,	-1,	5),
	}
};

// text
#define CR_NUM_TEXT			3
UI_XSTR Cr_text[GR_NUM_RESOLUTIONS][CR_NUM_TEXT] = {
	{ // GR_640
		{ "Restart",		1403,		569,	326, UI_XSTR_COLOR_GREEN,	-1, &Cr_buttons[0][CR_RESET_BUTTON].button },
		{ "Campaign",		1404,		569,	337, UI_XSTR_COLOR_GREEN,	-1, &Cr_buttons[0][CR_RESET_BUTTON].button },
		{ "Select",			1409,		568,	413, UI_XSTR_COLOR_PINK,	-1, &Cr_buttons[0][CR_COMMIT_BUTTON].button },
	},
	{ // GR_1024
		{ "Restart",		1403,		922,	523, UI_XSTR_COLOR_GREEN,	-1, &Cr_buttons[1][CR_RESET_BUTTON].button },
		{ "Campaign",		1404,		922,	538, UI_XSTR_COLOR_GREEN,	-1, &Cr_buttons[1][CR_RESET_BUTTON].button },
		{ "Select",			1409,		921,	665, UI_XSTR_COLOR_PINK,	-1, &Cr_buttons[1][CR_COMMIT_BUTTON].button },
	}
};

static int Desc_scroll_offset;
static int Selected_campaign_index;
static int Active_campaign_index;

const char *Info_text_ptrs[MAX_INFO_LINES];
int Num_info_lines, Info_text_line_size[MAX_INFO_LINES];

void campaign_room_build_listing()
{
	font::set_font(font::FONT1);
	int font_height = gr_get_font_height();
	int i, y;

	Num_lines = y = 0;

	for (i = 0; i < Num_campaigns; i++) {
		if (Campaign_names[i] != NULL) {
			// determine some extra information
			int flags = 0;
			auto fb = game_find_builtin_mission(Campaign_file_names[i]);
			if (fb != NULL) {
				if (fb->flags & FSB_FROM_VOLITION) {
					flags |= READYROOM_FLAG_FROM_VOLITION;
				}
			}

			sim_room_line_add(READYROOM_LINE_CAMPAIGN, Campaign_names[i], Campaign_file_names[i], Cr_list_coords[gr_screen.res][0], y, flags);
			y += font_height + 2;
		}
	}
}

void set_new_campaign_line(int n)
{
	char *str;

	Selected_campaign_index = n;
	str = Campaign_descs[Selected_campaign_index];
	Num_info_lines = 0;
	if (str) {
		Num_info_lines = split_str(str, Cr_info_coords[gr_screen.res][2], Info_text_line_size, Info_text_ptrs, MAX_INFO_LINES, MAX_INFO_LINE_LEN);
		Assert(Num_info_lines >= 0);
	}

	Desc_scroll_offset = 0;
}

void campaign_room_scroll_info_up()
{
	if (Desc_scroll_offset) {
		Desc_scroll_offset--;
		gamesnd_play_iface(InterfaceSounds::SCROLL);

	} else
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}

void campaign_room_scroll_info_down()
{
	if ( (Num_info_lines - Desc_scroll_offset) * gr_get_font_height() > Cr_info_coords[gr_screen.res][3]) {
		Desc_scroll_offset++;
		gamesnd_play_iface(InterfaceSounds::SCROLL);

	} else
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}

void campaign_reset(const SCP_string& campaign_file)
{
	// note: we do not toss all-time stats from player's performance in campaign up till now
	mission_campaign_savefile_delete(campaign_file.c_str());

	const int load_status = mission_campaign_load(campaign_file.c_str(), nullptr, nullptr, 1);

	// see if we successfully loaded this campaign
	if (load_status == 0) {
		// Goober5000 - reinitialize tech database if needed
		if ((Campaign.flags & CF_CUSTOM_TECH_DATABASE) || !stricmp(Campaign.filename, "freespace2")) {
			// reset tech database to what's in the tables
			tech_reset_to_default();

			// write the savefile so that we don't later load a stale techroom
			Pilot.save_savefile();
		}

		if (OnCampaignBeginHook->isActive()) {
			OnCampaignBeginHook->run(scripting::hook_param_list(scripting::hook_param("Campaign", 's', Campaign.filename)));
		}
	}

	mission_campaign_next_mission();
}

// returns: 0 = success, !0 = aborted or failed
int campaign_room_reset_campaign(int n)
{
	int z = popup(PF_TITLE_BIG | PF_TITLE_RED, 2, POPUP_CANCEL, POPUP_OK, XSTR( "Warning\nThis will cause all progress in your\ncurrent campaign to be lost", 110));
	if (z == 1) {
		campaign_reset(Campaign_file_names[n]);

		return 0;
	}

	return 1;
}

void campaign_room_commit()
{
	if (Selected_campaign_index < 0) {
		gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
		return;
	}

	campaign_select_campaign(Campaign_file_names[Selected_campaign_index]);

	gameseq_post_event(GS_EVENT_MAIN_MENU);
	gamesnd_play_iface(InterfaceSounds::COMMIT_PRESSED);
}

int campaign_room_button_pressed(int n)
{
	switch (n) {
		case CR_SCROLL_UP_BUTTON:
			sim_room_scroll_screen_up();
			break;

		case CR_SCROLL_DOWN_BUTTON:
			sim_room_scroll_screen_down();
			break;

		case CR_SCROLL_INFO_UP_BUTTON:
			campaign_room_scroll_info_up();
			break;

		case CR_SCROLL_INFO_DOWN_BUTTON:
			campaign_room_scroll_info_down();
			break;

		case CR_COMMIT_BUTTON:
			campaign_room_commit();
			break;

		/*
		case CR_HELP_BUTTON:
			launch_context_help();
			gamesnd_play_iface(SND_HELP_PRESSED);
			break;

		case CR_OPTIONS_BUTTON:
			gamesnd_play_iface(SND_SWITCH_SCREENS);
			gameseq_post_event(GS_EVENT_OPTIONS_MENU);
			return 1;
		*/

		case CR_RESET_BUTTON:
			if ( (Active_campaign_index < 0) || (Active_campaign_index >= Num_campaigns) )
				gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
			else if (campaign_room_reset_campaign(Active_campaign_index))
				gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
			else
				gamesnd_play_iface(InterfaceSounds::USER_SELECT);

			break;
	}

	return 0;
}

bool campaign_build_campaign_list() {
	const auto load_failed = mission_load_up_campaign();
	if (!load_failed) {
		mission_campaign_next_mission();
	} else {
		Campaign.filename[0] = 0;
		Campaign.num_missions = 0;

		mission_campaign_load_failure_popup();
	}

	// we need descriptions too, so "true" it
	mission_campaign_build_list(true);

	// convert to proper bool
	return load_failed != 0;
}

void campaign_select_campaign(const SCP_string& campaign_file)
{
	// new campaign selected?
	if (stricmp(campaign_file.c_str(), Campaign.filename) != 0) {
		strcpy_s(Player->current_campaign, campaign_file.c_str()); // track new campaign for player

		// attempt to load the campaign
		const int load_status = mission_campaign_load(campaign_file.c_str());

		// see if we successfully loaded this campaign and it's at the beginning
		if (load_status == 0 && Campaign.prev_mission < 0) {
			// Goober5000 - reinitialize tech database if needed
			if ((Campaign.flags & CF_CUSTOM_TECH_DATABASE) || !stricmp(Campaign.filename, "freespace2")) {
				// reset tech database to what's in the tables
				tech_reset_to_default();

				// write the savefile so that we don't later load a stale techroom
				Pilot.save_savefile();
			}

			if (OnCampaignBeginHook->isActive()) {
				OnCampaignBeginHook->run(scripting::hook_param_list(scripting::hook_param("Campaign", 's', Campaign.filename)));
			}
		}

		// that's all we need to do for now; the campaign loading status will be checked again when we try to load the
		// campaign in the ready room
	}
}

void campaign_room_init()
{
	int i;
	ui_button_info *b;

	list_h = Mission_list_coords[gr_screen.res][3];

	Ui_window.create(0, 0, gr_screen.max_w_unscaled, gr_screen.max_h_unscaled, 0);

	Campaign_background_bitmap_mask = bm_load(Campaign_mask_filename[gr_screen.res]);
	if (Campaign_background_bitmap_mask < 0) {
		Warning(LOCATION, "Error loading campaign room mask %s", Campaign_mask_filename[gr_screen.res]);
	} else {
		Ui_window.set_mask_bmap(Campaign_mask_filename[gr_screen.res]);
	}

	for (i=0; i<CR_NUM_BUTTONS; i++) {
		b = &Cr_buttons[gr_screen.res][i];

		b->button.create(&Ui_window, "", b->x, b->y, 60, 30, (i < 2), 1);
		// set up callback for when a mouse first goes over a button
		b->button.set_highlight_action(common_play_highlight_sound);
		b->button.set_bmaps(b->filename);
		b->button.link_hotspot(b->hotspot);
	}

	for (i=0; i<LIST_BUTTONS_MAX; i++) {
		List_buttons[i].create(&Ui_window, "", 0, 0, 60, 30, 0, 1);
		List_buttons[i].hide();
		List_buttons[i].disable();
	}

	// add xstrs
	for(i=0; i<CR_NUM_TEXT; i++){
		Ui_window.add_XSTR(&Cr_text[gr_screen.res][i]);
	}

	// set up sim_rooms for buttons so we draw the correct animation frame when a key is pressed
	Cr_buttons[gr_screen.res][CR_SCROLL_UP_BUTTON].button.set_hotkey(KEY_PAGEUP);
	Cr_buttons[gr_screen.res][CR_SCROLL_DOWN_BUTTON].button.set_hotkey(KEY_PAGEDOWN);
	Cr_buttons[gr_screen.res][CR_RESET_BUTTON].button.set_hotkey(KEY_DELETE);
	Cr_buttons[gr_screen.res][CR_COMMIT_BUTTON].button.set_hotkey(KEY_CTRLED | KEY_ENTER);
	// Cr_buttons[gr_screen.res][CR_HELP_BUTTON].button.set_hotkey(KEY_F2);

	Background_bitmap = bm_load(Campaign_filename[gr_screen.res]);
	if (Background_bitmap < 0) {
		Warning(LOCATION, "Error loading campaign room background %s", Campaign_filename[gr_screen.res]);
	}

	// load in help overlay bitmap	
	Campaign_room_overlay_id = help_overlay_get_index(CAMPAIGN_ROOM_OVERLAY);
	help_overlay_set_state(Campaign_room_overlay_id,gr_screen.res,0);

	Desc_scroll_offset = Scroll_offset = 0;

	// this stuff needs to happen before the mission_campaign_build_list() call
	const auto load_failed = campaign_build_campaign_list();

	campaign_room_build_listing();

	Selected_campaign_index = Active_campaign_index = -1;
	if (!load_failed) {
		for (i=0; i<Num_campaigns; i++)
			if (!stricmp(Campaign_file_names[i], Campaign.filename)) {
				set_new_campaign_line(i);
				Active_campaign_index = i;
				break;
			}
	}

	Campaign_room_no_campaigns = false;
}

void campaign_room_close()
{
	if (bm_is_valid(Background_bitmap))
		bm_release(Background_bitmap);

	// Reset info text pointers and size since they may contain pointers into the campaign description which will be
	// freed soon.
	memset(Info_text_line_size, 0, sizeof(Info_text_line_size));
	memset(Info_text_ptrs, 0, sizeof(Info_text_ptrs));

	// free the global Campaign_* list stuff
	mission_campaign_free_list();

	Ui_window.destroy();

	if (bm_is_valid(Campaign_background_bitmap_mask)) {
		bm_release(Campaign_background_bitmap_mask);
	}

	common_free_interface_palette();		// restore game palette
	Pilot.save_player();
}

void campaign_room_do_frame(float  /*frametime*/)
{
	char buf[256];
	char line_text[MAX_INFO_LINE_LEN];
	int i, k, y, line;
	int select_tease_line = -1;  // line mouse is down on, but won't be selected until button released

	// If we don't have a mask, we don't have enough data to do anything with this screen.
	if (Campaign_background_bitmap_mask == -1) {
		if ((Active_campaign_index < 0) || (Active_campaign_index >= Num_campaigns)) {
			// Player is trying to use a regular pilot in the demo.
			if (Num_campaigns < 1) {
				// If there are no campaigns loaded, there's really nothing left to do.
				popup_game_feature_not_in_demo();
				return;
			}
			int select_default = popup(PF_USE_AFFIRMATIVE_ICON|PF_BODY_BIG, 2, "Exit", "Select first campaign", "Campaign Room only available in full version. However, you may select the first campaign found.");
			if (select_default == 1) {
				campaign_select_campaign(Campaign_file_names[0]);
			}
		} else {
			int reset_campaign = popup(PF_USE_AFFIRMATIVE_ICON|PF_BODY_BIG, 2, "Exit", "Restart Campaign", "Campaign Room only available in full version. However, you may restart the campaign.");
			if (reset_campaign == 1) {
				// Rather than hardcoding the reset, let's reuse this.
				campaign_room_reset_campaign(Active_campaign_index);
			}
		}
		gameseq_post_event(GS_EVENT_MAIN_MENU);
		return;
	}

	if ( help_overlay_active(Campaign_room_overlay_id) ) {
		// Cr_buttons[gr_screen.res][CR_HELP_BUTTON].button.reset_status();
		Ui_window.set_ignore_gadgets(1);
	}

	k = Ui_window.process() & ~KEY_DEBUGGED;

	if ( (k > 0) || B1_JUST_RELEASED ) {
		if ( help_overlay_active(Campaign_room_overlay_id) ) {
			help_overlay_set_state(Campaign_room_overlay_id, gr_screen.res, 0);
			Ui_window.set_ignore_gadgets(0);
			k = 0;
		}
	}

	if ( !help_overlay_active(Campaign_room_overlay_id) ) {
		Ui_window.set_ignore_gadgets(0);
	}

	switch (k) {
		case KEY_DOWN:  // scroll list down
			if (Selected_campaign_index < Num_campaigns - 1) {
				set_new_campaign_line(Selected_campaign_index + 1);
				gamesnd_play_iface(InterfaceSounds::SCROLL);

			} else
				gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);

			break;

		case KEY_UP:  // scroll list up
			if (Selected_campaign_index < 0)
				Selected_campaign_index = 1;

			if (Selected_campaign_index) {
				set_new_campaign_line(Selected_campaign_index - 1);
				gamesnd_play_iface(InterfaceSounds::SCROLL);

			} else
				gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);

			break;

		case KEY_ESC:
			gameseq_post_event(GS_EVENT_MAIN_MENU);
			break;
	}	// end switch

	for (i=0; i<CR_NUM_BUTTONS; i++){
		if (Cr_buttons[gr_screen.res][i].button.pressed()){
			if (campaign_room_button_pressed(i)){
				return;
			}
		}
	}

	for (i=0; i<LIST_BUTTONS_MAX; i++) {
		if (List_buttons[i].is_mouse_on()){
			select_tease_line = i + Scroll_offset;
		}
	
		if (List_buttons[i].pressed()) {
			set_new_campaign_line(i + Scroll_offset);
			gamesnd_play_iface(InterfaceSounds::USER_SELECT);
		}
	}

	GR_MAYBE_CLEAR_RES(Background_bitmap);
	if (Background_bitmap >= 0) {
		gr_set_bitmap(Background_bitmap);
		gr_bitmap(0, 0, GR_RESIZE_MENU);
	}

	Ui_window.draw();

	font::set_font(font::FONT1);
	int font_height = gr_get_font_height();
	line = Scroll_offset;
	while (sim_room_line_query_visible(line) && (line - Scroll_offset < LIST_BUTTONS_MAX)) {
		y = Cr_list_coords[gr_screen.res][1] + sim_room_lines[line].y - sim_room_lines[Scroll_offset].y;

		List_buttons[line - Scroll_offset].update_dimensions(Cr_list_coords[gr_screen.res][0], y, Cr_list_coords[gr_screen.res][2], font_height);
		List_buttons[line - Scroll_offset].enable();

		if (!stricmp(sim_room_lines[line].filename, Campaign.filename)) {
			gr_set_color_fast(&Color_white);
			i = y + font_height / 2 - 1;
			gr_circle(Cr_list_coords[gr_screen.res][0] - 6, i, 5, GR_RESIZE_MENU);

			gr_set_color_fast(&Color_bright_white);
			gr_line(Cr_list_coords[gr_screen.res][0] - 10, i, Cr_list_coords[gr_screen.res][0] - 8, i, GR_RESIZE_MENU);
			gr_line(Cr_list_coords[gr_screen.res][0] - 6, i - 4, Cr_list_coords[gr_screen.res][0] - 6, i - 2, GR_RESIZE_MENU);
			gr_line(Cr_list_coords[gr_screen.res][0] - 4, i, Cr_list_coords[gr_screen.res][0] - 2, i, GR_RESIZE_MENU);
			gr_line(Cr_list_coords[gr_screen.res][0] - 6, i + 2, Cr_list_coords[gr_screen.res][0] - 6, i + 4, GR_RESIZE_MENU);
		}

		if (line == Selected_campaign_index)
			gr_set_color_fast(&Color_text_selected);
		else if (line == select_tease_line)
			gr_set_color_fast(&Color_text_subselected);
		else
			gr_set_color_fast(&Color_text_normal);

		strcpy_s(buf, sim_room_lines[line].name);
		font::force_fit_string(buf, 255, Cr_list_coords[gr_screen.res][0] + Cr_list_coords[gr_screen.res][2] - sim_room_lines[line].x);
		gr_printf_menu(sim_room_lines[line].x, y, "%s", buf);
		line++;
	}

	i = line - Scroll_offset;
	while (i < LIST_BUTTONS_MAX)
		List_buttons[i++].disable();

	y = 0;
	i = Desc_scroll_offset;
	gr_set_color_fast(&Color_text_normal);

	while (y + font_height <= Cr_info_coords[gr_screen.res][3]) {
		if (i >= Num_info_lines)
			break;

		Assert(Info_text_line_size[i] < MAX_INFO_LINE_LEN);
		strncpy(line_text, Info_text_ptrs[i], Info_text_line_size[i]);
		line_text[Info_text_line_size[i]] = 0;
		drop_white_space(line_text);
		gr_string(Cr_info_coords[gr_screen.res][0], Cr_info_coords[gr_screen.res][1] + y, line_text, GR_RESIZE_MENU);
		y += font_height;
		i++;
	}

	if (Num_campaigns < 1) {
		popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, XSTR( "No campaigns are available!", 1613));
		Campaign_room_no_campaigns = true;
		gameseq_post_event(GS_EVENT_MAIN_MENU);
	}

	// blit help overlay if active
	help_overlay_maybe_blit(Campaign_room_overlay_id, gr_screen.res);

	gr_flip();
}

void sim_room_load_mission_icons()
{
	int idx;
	
	// load all bitmaps
	for(idx=0; idx<NUM_MISSION_ICONS; idx++){
		Mission_icon_bitmaps[idx] = -1;
		Mission_icon_bitmaps[idx] = bm_load(Mission_icon_bitmap_filenames[idx]);
	}
}

void sim_room_unload_mission_icons()
{
	int idx;

	// unload all bitmaps
	for(idx=0; idx<NUM_MISSION_ICONS; idx++){
		if(Mission_icon_bitmaps[idx] >= 0){
			// don't bm_release() here, used in multiple places - taylor
			bm_unload(Mission_icon_bitmaps[idx]);
			Mission_icon_bitmaps[idx] = -1;
		}
	}
}
