Commit message: Fix integer overflow in layer_resize in map_engine.c (#268)

* Fix integer overflow in layer_resize in map_engine.c

There's a buffer overflow bug in the function layer_resize. It allocates
a buffer `tilemap` with size `x_size * y_size * sizeof(struct map_tile)`.
But it didn't check for integer overflow, so if x_size and y_size are
very large, it's possible that the buffer size is smaller than needed,
causing a buffer overflow later.

PoC: `SetLayerSize(0, 0x7FFFFFFF, 0x7FFFFFFF);`

* move malloc to a separate line
CVE: CVE-2018-1000524
CWE: CWE-190
Vulnerability description: miniSphere version 5.2.9 and earlier contains a Integer Overflow vulnerability in layer_resize() function in map_engine.c that can result in remote denial of service. This attack appear to be exploitable via the victim must load a specially-crafted map which calls SetLayerSize in its entry script. This vulnerability appears to have been fixed in 5.0.3, 5.1.5, 5.2.10 and later.

Project: minisphere

change_map(const char* filename, bool preserve_persons)
{

	struct map*        map;
	person_t*          person;
	struct map_person* person_info;
	path_t*            path;
	spriteset_t*       spriteset = NULL;

	int i;

	console_log(2, "changing current map to '%s'", filename);

	map = load_map(filename);
	if (map == NULL) return false;
	if (s_map != NULL) {
		map_activate(MAP_SCRIPT_ON_LEAVE, true);
	}

	free_map(s_map); free(s_map_filename);
	for (i = 0; i < s_num_deferreds; ++i)
		script_unref(s_deferreds[i].script);
	s_num_deferreds = 0;
	s_map = map; s_map_filename = strdup(filename);
	reset_persons(preserve_persons);

	for (i = 0; i < s_map->num_persons; ++i) {
		person_info = &s_map->persons[i];
		path = game_full_path(g_game, lstr_cstr(person_info->spriteset), "spritesets", true);
		spriteset = spriteset_load(path_cstr(path));
		path_free(path);
		if (spriteset == NULL)
			goto on_error;
		if (!(person = person_new(lstr_cstr(person_info->name), spriteset, false, NULL)))
			goto on_error;
		spriteset_unref(spriteset);
		person_set_xyz(person, person_info->x, person_info->y, person_info->z);
		person_compile_script(person, PERSON_SCRIPT_ON_CREATE, person_info->create_script);
		person_compile_script(person, PERSON_SCRIPT_ON_DESTROY, person_info->destroy_script);
		person_compile_script(person, PERSON_SCRIPT_ON_TOUCH, person_info->touch_script);
		person_compile_script(person, PERSON_SCRIPT_ON_TALK, person_info->talk_script);
		person_compile_script(person, PERSON_SCRIPT_GENERATOR, person_info->command_script);

		person_activate(person, PERSON_SCRIPT_ON_CREATE, NULL, false);
	}

	s_camera_x = s_map->origin.x;
	s_camera_y = s_map->origin.y;

	if (s_map->bgm_file == NULL && s_map_bgm_stream != NULL) {
		sound_unref(s_map_bgm_stream);
		lstr_free(s_last_bgm_file);
		s_map_bgm_stream = NULL;
		s_last_bgm_file = NULL;
	}
	else if (s_map->bgm_file != NULL
		&& (s_last_bgm_file == NULL || lstr_cmp(s_map->bgm_file, s_last_bgm_file) != 0))
	{
		sound_unref(s_map_bgm_stream);
		lstr_free(s_last_bgm_file);
		s_last_bgm_file = lstr_dup(s_map->bgm_file);
		path = game_full_path(g_game, lstr_cstr(s_map->bgm_file), "sounds", true);
		if ((s_map_bgm_stream = sound_new(path_cstr(path)))) {
			sound_set_repeat(s_map_bgm_stream, true);
			sound_play(s_map_bgm_stream, s_bgm_mixer);
		}
		path_free(path);
	}

	map_activate(MAP_SCRIPT_ON_ENTER, true);

	s_frames = 0;
	return true;

on_error:
	spriteset_unref(spriteset);
	free_map(s_map);
	return false;
}

Project: minisphere

command_person(person_t* person, int command)
{
	double    new_x;
	double    new_y;
	person_t* person_to_touch;

	new_x = person->x;
	new_y = person->y;
	switch (command) {
	case COMMAND_ANIMATE:
		person->revert_frames = person->revert_delay;
		if (person->anim_frames > 0 && --person->anim_frames == 0) {
			++person->frame;
			person->anim_frames = spriteset_frame_delay(person->sprite, person->direction, person->frame);
		}
		break;
	case COMMAND_FACE_NORTH:
		person_set_pose(person, "north");
		break;
	case COMMAND_FACE_NORTHEAST:
		person_set_pose(person, "northeast");
		break;
	case COMMAND_FACE_EAST:
		person_set_pose(person, "east");
		break;
	case COMMAND_FACE_SOUTHEAST:
		person_set_pose(person, "southeast");
		break;
	case COMMAND_FACE_SOUTH:
		person_set_pose(person, "south");
		break;
	case COMMAND_FACE_SOUTHWEST:
		person_set_pose(person, "southwest");
		break;
	case COMMAND_FACE_WEST:
		person_set_pose(person, "west");
		break;
	case COMMAND_FACE_NORTHWEST:
		person_set_pose(person, "northwest");
		break;
	case COMMAND_MOVE_NORTH:
		new_y = person->y - person->speed_y;
		break;
	case COMMAND_MOVE_EAST:
		new_x = person->x + person->speed_x;
		break;
	case COMMAND_MOVE_SOUTH:
		new_y = person->y + person->speed_y;
		break;
	case COMMAND_MOVE_WEST:
		new_x = person->x - person->speed_x;
		break;
	}
	if (new_x != person->x || new_y != person->y) {
		if (!person_obstructed_at(person, new_x, new_y, &person_to_touch, NULL)) {
			if (new_x != person->x)
				person->mv_x = new_x > person->x ? 1 : -1;
			if (new_y != person->y)
				person->mv_y = new_y > person->y ? 1 : -1;
			person->x = new_x;
			person->y = new_y;
		}
		else {
			if (person_to_touch != NULL)
				person_activate(person_to_touch, PERSON_SCRIPT_ON_TOUCH, person, true);
		}
	}
}

Project: minisphere

compare_persons(const void* a, const void* b)
{
	person_t* p1 = *(person_t**)a;
	person_t* p2 = *(person_t**)b;

	double x, y_p1, y_p2;
	int    y_delta;

	person_get_xy(p1, &x, &y_p1, true);
	person_get_xy(p2, &x, &y_p2, true);
	y_delta = y_p1 - y_p2;
	if (y_delta != 0)
		return y_delta;
	else if (person_following(p1, p2))
		return -1;
	else if (person_following(p2, p1))
		return 1;
	else
		return p1->id - p2->id;
}

Project: minisphere

detach_person(const person_t* person)
{
	int i;

	if (s_camera_person == person)
		s_camera_person = NULL;
	for (i = 0; i < PLAYER_MAX; ++i) {
		if (s_players[i].person == person)
			s_players[i].person = NULL;
	}
}

Project: minisphere

does_person_exist(const person_t* person)
{
	int i;

	for (i = 0; i < s_num_persons; ++i)
		if (person == s_persons[i]) return true;
	return false;
}

Project: minisphere

draw_persons(int layer, bool is_flipped, int cam_x, int cam_y)
{
	person_t*    person;
	spriteset_t* sprite;
	int          w, h;
	double       x, y;
	int          i;

	for (i = 0; i < s_num_persons; ++i) {
		person = s_persons[i];
		if (!person->is_visible || person->layer != layer)
			continue;
		sprite = person->sprite;
		w = spriteset_width(sprite);
		h = spriteset_height(sprite);
		person_get_xy(person, &x, &y, true);
		x -= cam_x - person->x_offset;
		y -= cam_y - person->y_offset;
		spriteset_draw(sprite, person->mask, is_flipped, person->theta, person->scale_x, person->scale_y,
			person->direction, trunc(x), trunc(y), person->frame);
	}
}

Project: minisphere

enlarge_step_history(person_t* person, int new_size)
{
	struct step *new_steps;
	size_t      pastmost;
	double      last_x;
	double      last_y;

	int i;

	if (new_size > person->max_history) {
		if (!(new_steps = realloc(person->steps, new_size * sizeof(struct step))))
			return false;

		pastmost = person->max_history - 1;
		last_x = person->steps != NULL ? person->steps[pastmost].x : person->x;
		last_y = person->steps != NULL ? person->steps[pastmost].y : person->y;
		for (i = person->max_history; i < new_size; ++i) {
			new_steps[i].x = last_x;
			new_steps[i].y = last_y;
		}
		person->steps = new_steps;
		person->max_history = new_size;
	}

	return true;
}

Project: minisphere

free_map(struct map* map)
{
	struct map_trigger* trigger;
	struct map_zone*    zone;

	iter_t iter;
	int    i;

	if (map == NULL)
		return;
	for (i = 0; i < MAP_SCRIPT_MAX; ++i)
		script_unref(map->scripts[i]);
	for (i = 0; i < map->num_layers; ++i) {
		script_unref(map->layers[i].render_script);
		lstr_free(map->layers[i].name);
		free(map->layers[i].tilemap);
		obsmap_free(map->layers[i].obsmap);
	}
	for (i = 0; i < map->num_persons; ++i) {
		lstr_free(map->persons[i].name);
		lstr_free(map->persons[i].spriteset);
		lstr_free(map->persons[i].create_script);
		lstr_free(map->persons[i].destroy_script);
		lstr_free(map->persons[i].command_script);
		lstr_free(map->persons[i].talk_script);
		lstr_free(map->persons[i].touch_script);
	}
	iter = vector_enum(s_map->triggers);
	while ((trigger = iter_next(&iter)))
		script_unref(trigger->script);
	iter = vector_enum(s_map->zones);
	while ((zone = iter_next(&iter)))
		script_unref(zone->script);
	lstr_free(s_map->bgm_file);
	tileset_free(map->tileset);
	free(map->layers);
	free(map->persons);
	vector_free(map->triggers);
	vector_free(map->zones);
	free(map);
}

Project: minisphere

free_person(person_t* person)
{
	int i;

	free(person->steps);
	for (i = 0; i < PERSON_SCRIPT_MAX; ++i)
		script_unref(person->scripts[i]);
	spriteset_unref(person->sprite);
	free(person->commands);
	free(person->name);
	free(person->direction);
	free(person);
}

Project: minisphere

get_zone_at(int x, int y, int layer, int which, int* out_index)
{
	struct map_zone* found_item = NULL;
	struct map_zone* zone;

	iter_t iter;
	int    i;

	iter = vector_enum(s_map->zones); i = -1;
	while ((zone = iter_next(&iter))) {
		if (zone->layer != layer && false)  // layer ignored for compatibility
			continue;
		if (is_point_in_rect(x, y, zone->bounds) && which-- == 0) {
			found_item = zone;
			if (out_index) *out_index = (int)iter.index;
			break;
		}
	}
	return found_item;
}

Project: minisphere

layer_get_color_mask(int layer)
{
	return s_map->layers[layer].color_mask;
}

Project: minisphere

layer_get_reflective(int layer)
{
	return s_map->layers[layer].is_reflective;
}

Project: minisphere

layer_get_tile(int layer, int x, int y)
{
	struct map_tile* tile;
	int              width;

	width = s_map->layers[layer].width;
	tile = &s_map->layers[layer].tilemap[x + y * width];
	return tile->tile_index;
}

Project: minisphere

layer_name(int layer)
{
	return lstr_cstr(s_map->layers[layer].name);
}

Project: minisphere

layer_obsmap(int layer)
{
	return s_map->layers[layer].obsmap;
}

Project: minisphere

layer_on_render(int layer, script_t* script)
{
	script_unref(s_map->layers[layer].render_script);
	s_map->layers[layer].render_script = script_ref(script);
}

Project: minisphere

layer_replace_tiles(int layer, int old_index, int new_index)
{
	int              layer_h;
	int              layer_w;
	struct map_tile* tile;

	int i_x, i_y;

	layer_w = s_map->layers[layer].width;
	layer_h = s_map->layers[layer].height;
	for (i_x = 0; i_x < layer_w; ++i_x) for (i_y = 0; i_y < layer_h; ++i_y) {
		tile = &s_map->layers[layer].tilemap[i_x + i_y * layer_w];
		if (tile->tile_index == old_index)
			tile->tile_index = new_index;
	}
}

Project: minisphere

layer_set_color_mask(int layer, color_t color)
{
	s_map->layers[layer].color_mask = color;
}

Project: minisphere

layer_set_reflective(int layer, bool reflective)
{
	s_map->layers[layer].is_reflective = reflective;
}

Project: minisphere

layer_set_tile(int layer, int x, int y, int tile_index)
{
	struct map_tile* tile;
	int              width;

	width = s_map->layers[layer].width;
	tile = &s_map->layers[layer].tilemap[x + y * width];
	tile->tile_index = tile_index;
	tile->frames_left = tileset_get_delay(s_map->tileset, tile_index);
}

Project: minisphere

layer_size(int layer)
{
	struct map_layer* layer_data;

	layer_data = &s_map->layers[layer];
	return mk_size2(layer_data->width, layer_data->height);
}

Project: minisphere

map_activate(map_op_t op, bool use_default)
{
	if (use_default)
		script_run(s_def_map_scripts[op], false);
	script_run(s_map->scripts[op], false);
}

Project: minisphere

map_add_trigger(int x, int y, int layer, script_t* script)
{
	struct map_trigger trigger;

	console_log(2, "creating trigger #%d on map '%s'", vector_len(s_map->triggers), s_map_filename);
	console_log(3, "    location: '%s' @ (%d,%d)", lstr_cstr(s_map->layers[layer].name), x, y);

	trigger.x = x; trigger.y = y;
	trigger.z = layer;
	trigger.script = script_ref(script);
	if (!vector_push(s_map->triggers, &trigger))
		return false;
	return true;
}

Project: minisphere

map_add_zone(rect_t bounds, int layer, script_t* script, int steps)
{
	struct map_zone zone;

	console_log(2, "creating %u-step zone #%d on map '%s'", steps, vector_len(s_map->zones), s_map_filename);
	console_log(3, "    bounds: (%d,%d)-(%d,%d)", bounds.x1, bounds.y1, bounds.x2, bounds.y2);

	memset(&zone, 0, sizeof(struct map_zone));
	zone.bounds = bounds;
	zone.layer = layer;
	zone.script = script_ref(script);
	zone.interval = steps;
	zone.steps_left = 0;
	if (!vector_push(s_map->zones, &zone))
		return false;
	return true;
}

Project: minisphere

map_bounds(void)
{
	rect_t bounds;
	int    tile_w, tile_h;

	tileset_get_size(s_map->tileset, &tile_w, &tile_h);
	bounds.x1 = 0; bounds.y1 = 0;
	bounds.x2 = s_map->width * tile_w;
	bounds.y2 = s_map->height * tile_h;
	return bounds;
}

Project: minisphere

map_call_default(map_op_t op)
{
	script_run(s_def_map_scripts[op], false);
}

Project: minisphere

map_engine_acting_person(void)
{
	return s_acting_person;
}

Project: minisphere

map_engine_active_person(void)
{
	return s_current_person;
}

Project: minisphere

map_engine_active_trigger(void)
{
	return s_current_trigger;
}

Project: minisphere

map_engine_active_zone(void)
{
	return s_current_zone;
}

Project: minisphere

map_engine_change_map(const char* filename)
{
	return change_map(filename, false);
}

Project: minisphere

map_engine_defer(script_t* script, int num_frames)
{
	struct deferred* deferred;

	if (++s_num_deferreds > s_max_deferreds) {
		s_max_deferreds = s_num_deferreds * 2;
		s_deferreds = realloc(s_deferreds, s_max_deferreds * sizeof(struct deferred));
	}
	deferred = &s_deferreds[s_num_deferreds - 1];
	deferred->script = script;
	deferred->frames_left = num_frames;
}

Project: minisphere

map_engine_draw_map(void)
{
	bool              is_repeating;
	int               cell_x;
	int               cell_y;
	int               first_cell_x;
	int               first_cell_y;
	struct map_layer* layer;
	int               layer_height;
	int               layer_width;
	size2_t           resolution;
	int               tile_height;
	int               tile_index;
	int               tile_width;
	int               off_x;
	int               off_y;

	int x, y, z;

	if (screen_skipping_frame(g_screen))
		return;

	resolution = screen_size(g_screen);
	tileset_get_size(s_map->tileset, &tile_width, &tile_height);

	for (z = 0; z < s_map->num_layers; ++z) {
		layer = &s_map->layers[z];
		is_repeating = s_map->is_repeating || layer->is_parallax;
		layer_width = layer->width * tile_width;
		layer_height = layer->height * tile_height;
		off_x = 0;
		off_y = 0;
		map_screen_to_layer(z, s_camera_x, s_camera_y, &off_x, &off_y);

		al_hold_bitmap_drawing(true);
		if (layer->is_reflective) {
			if (is_repeating) {  // for small repeating maps, persons need to be repeated as well
				for (y = 0; y < resolution.height / layer_height + 2; ++y) for (x = 0; x < resolution.width / layer_width + 2; ++x)
					draw_persons(z, true, off_x - x * layer_width, off_y - y * layer_height);
			}
			else {
				draw_persons(z, true, off_x, off_y);
			}
		}

		if (layer->is_visible) {
			first_cell_x = off_x / tile_width;
			first_cell_y = off_y / tile_height;
			for (y = 0; y < resolution.height / tile_height + 2; ++y) for (x = 0; x < resolution.width / tile_width + 2; ++x) {
				cell_x = is_repeating ? (x + first_cell_x) % layer->width : x + first_cell_x;
				cell_y = is_repeating ? (y + first_cell_y) % layer->height : y + first_cell_y;
				if (cell_x < 0 || cell_x >= layer->width || cell_y < 0 || cell_y >= layer->height)
					continue;
				tile_index = layer->tilemap[cell_x + cell_y * layer->width].tile_index;
				tileset_draw(s_map->tileset, layer->color_mask, x * tile_width - off_x % tile_width, y * tile_height - off_y % tile_height, tile_index);
			}
		}

		if (is_repeating) {  // for small repeating maps, persons need to be repeated as well
			for (y = 0; y < resolution.height / layer_height + 2; ++y) for (x = 0; x < resolution.width / layer_width + 2; ++x)
				draw_persons(z, false, off_x - x * layer_width, off_y - y * layer_height);
		}
		else {
			draw_persons(z, false, off_x, off_y);
		}
		al_hold_bitmap_drawing(false);

		script_run(layer->render_script, false);
	}

	al_draw_filled_rectangle(0, 0, resolution.width, resolution.height, nativecolor(s_color_mask));
	script_run(s_render_script, false);
}

Project: minisphere

map_engine_exit(void)
{
	s_exiting = true;
}

Project: minisphere

map_engine_fade_to(color_t color_mask, int num_frames)
{
	if (num_frames > 0) {
		s_fade_color_to = color_mask;
		s_fade_color_from = s_color_mask;
		s_fade_frames = num_frames;
		s_fade_progress = 0;
	}
	else {
		s_color_mask = color_mask;
		s_fade_color_to = s_fade_color_from = color_mask;
		s_fade_progress = s_fade_frames = 0;
	}
}

Project: minisphere

map_engine_get_framerate(void)
{
	return s_frame_rate;
}

Project: minisphere

map_engine_get_player(player_id_t player_id)
{
	return s_players[player_id].person;
}

Project: minisphere

map_engine_get_subject(void)
{
	return s_camera_person;
}

Project: minisphere

map_engine_get_talk_button(void)
{
	return s_talk_button;
}

Project: minisphere

map_engine_get_talk_distance(void)
{
	return s_talk_distance;
}

Project: minisphere

map_engine_get_talk_key(player_id_t player_id)
{
	return s_players[player_id].talk_key;
}

Project: minisphere

map_engine_init(void)
{
	int i;

	console_log(1, "initializing map engine subsystem");

	audio_init();
	s_bgm_mixer = mixer_new(44100, 16, 2);

	memset(s_def_map_scripts, 0, MAP_SCRIPT_MAX * sizeof(int));
	memset(s_def_person_scripts, 0, PERSON_SCRIPT_MAX * sizeof(int));
	s_map = NULL; s_map_filename = NULL;
	s_camera_person = NULL;
	s_players = calloc(PLAYER_MAX, sizeof(struct player));
	for (i = 0; i < PLAYER_MAX; ++i)
		s_players[i].is_talk_allowed = true;
	s_current_trigger = -1;
	s_current_zone = -1;
	s_render_script = NULL;
	s_update_script = NULL;
	s_num_deferreds = s_max_deferreds = 0;
	s_deferreds = NULL;
	s_talk_button = 0;
	s_is_map_running = false;
	s_color_mask = mk_color(0, 0, 0, 0);
	s_on_trigger = NULL;

	s_num_persons = s_max_persons = 0;
	s_persons = NULL;
	s_talk_distance = 8;
	s_acting_person = NULL;
	s_current_person = NULL;
}

Project: minisphere

map_engine_on_map_event(map_op_t op, script_t* script)
{
	script_t* old_script;

	old_script = s_def_map_scripts[op];
	s_def_map_scripts[op] = script_ref(script);
	script_unref(old_script);
}

Project: minisphere

map_engine_on_person_event(person_op_t op, script_t* script)
{
	script_t* old_script;

	old_script = s_def_person_scripts[op];
	s_def_person_scripts[op] = script_ref(script);
	script_unref(old_script);
}

Project: minisphere

map_engine_on_render(script_t* script)
{
	script_unref(s_render_script);
	s_render_script = script_ref(script);
}

Project: minisphere

map_engine_persons(void)
{
	int i;

	if (s_person_list == NULL)
		s_person_list = vector_new(sizeof(person_t*));
	vector_clear(s_person_list);
	for (i = 0; i < s_num_persons; ++i)
		vector_push(s_person_list, &s_persons[i]);
	return s_person_list;
}

Project: minisphere

map_engine_running(void)
{
	return s_is_map_running;
}

Project: minisphere

map_engine_set_framerate(int framerate)
{
	s_frame_rate = framerate;
}

Project: minisphere

map_engine_set_player(player_id_t player_id, person_t* person)
{
	int i;

	for (i = 0; i < PLAYER_MAX; ++i) {
		if (s_players[i].person == person)
			s_players[i].person = NULL;
	}

	s_players[player_id].person = person;
}

Project: minisphere

map_engine_set_subject(person_t* person)
{
	s_camera_person = person;
}

Project: minisphere

map_engine_set_talk_button(int button_id)
{
	s_talk_button = button_id;
}

Project: minisphere

map_engine_set_talk_distance(int distance)
{
	s_talk_distance = distance;
}

Project: minisphere

map_engine_set_talk_key(player_id_t player_id, int key)
{
	s_players[player_id].talk_key = key;
}

Project: minisphere

map_engine_start(const char* filename, int framerate)
{
	s_is_map_running = true;
	s_exiting = false;
	s_color_mask = mk_color(0, 0, 0, 0);
	s_fade_color_to = s_fade_color_from = s_color_mask;
	s_fade_progress = s_fade_frames = 0;
	al_clear_to_color(al_map_rgba(0, 0, 0, 255));
	s_frame_rate = framerate;
	if (!change_map(filename, true))
		goto on_error;
	while (!s_exiting && jsal_vm_enabled()) {
		sphere_heartbeat(true, 1);

		update_map_engine(true);
		process_map_input();
		map_engine_draw_map();

		sphere_tick(1, false, s_frame_rate);
	}
	reset_persons(false);
	s_is_map_running = false;
	return true;

on_error:
	s_is_map_running = false;
	return false;
}

Project: minisphere

map_engine_uninit(void)
{
	int i;

	console_log(1, "shutting down map engine subsystem");

	vector_free(s_person_list);

	for (i = 0; i < s_num_deferreds; ++i)
		script_unref(s_deferreds[i].script);
	free(s_deferreds);
	for (i = 0; i < MAP_SCRIPT_MAX; ++i)
		script_unref(s_def_map_scripts[i]);
	script_unref(s_update_script);
	script_unref(s_render_script);
	free_map(s_map);
	free(s_players);

	for (i = 0; i < s_num_persons; ++i)
		free_person(s_persons[i]);
	for (i = 0; i < PERSON_SCRIPT_MAX; ++i)
		script_unref(s_def_person_scripts[i]);
	free(s_persons);

	mixer_unref(s_bgm_mixer);

	audio_uninit();
}

Project: minisphere

map_engine_update(void)
{
	update_map_engine(false);
}

Project: minisphere

map_get_camera_xy(void)
{
	return mk_point2(s_camera_x, s_camera_y);
}

Project: minisphere

map_layer_by_name(const char* name)
{
	int i;

	for (i = 0; i < s_map->num_layers; ++i) {
		if (strcmp(name, lstr_cstr(s_map->layers[0].name)) == 0)
			return i;
	}
	return -1;
}

Project: minisphere

map_normalize_xy(double* inout_x, double* inout_y, int layer)
{
	int tile_w, tile_h;
	int layer_w, layer_h;

	if (s_map == NULL)
		return;  // can't normalize if no map loaded
	if (!s_map->is_repeating && !s_map->layers[layer].is_parallax)
		return;
	tileset_get_size(s_map->tileset, &tile_w, &tile_h);
	layer_w = s_map->layers[layer].width * tile_w;
	layer_h = s_map->layers[layer].height * tile_h;
	if (inout_x)
		*inout_x = fmod(fmod(*inout_x, layer_w) + layer_w, layer_w);
	if (inout_y)
		*inout_y = fmod(fmod(*inout_y, layer_h) + layer_h, layer_h);
}

Project: minisphere

map_num_layers(void)
{
	return s_map->num_layers;
}

Project: minisphere

map_num_persons(void)
{
	return s_num_persons;
}

Project: minisphere

map_num_triggers(void)
{
	return vector_len(s_map->triggers);
}

Project: minisphere

map_num_zones(void)
{
	return vector_len(s_map->zones);
}

Project: minisphere

map_pathname(void)
{
	return s_map ? s_map_filename : NULL;
}

Project: minisphere

map_person_by_name(const char* name)
{
	int i;

	for (i = 0; i < s_num_persons; ++i) {
		if (strcmp(name, s_persons[i]->name) == 0)
			return s_persons[i];
	}
	return NULL;
}

Project: minisphere

map_remove_zone(int zone_index)
{
	vector_remove(s_map->zones, zone_index);
}

Project: minisphere

map_screen_to_layer(int layer, int camera_x, int camera_y, int* inout_x, int* inout_y)
{
	rect_t  bounds;
	int     center_x;
	int     center_y;
	int     layer_h;
	int     layer_w;
	float   plx_offset_x = 0.0;
	int     plx_offset_y = 0.0;
	size2_t resolution;
	int     tile_w;
	int     tile_h;
	int     x_offset;
	int     y_offset;

	resolution = screen_size(g_screen);
	tileset_get_size(s_map->tileset, &tile_w, &tile_h);
	layer_w = s_map->layers[layer].width * tile_w;
	layer_h = s_map->layers[layer].height * tile_h;
	center_x = resolution.width / 2;
	center_y = resolution.height / 2;

	if (!s_map->is_repeating) {
		bounds = map_bounds();
		camera_x = fmin(fmax(camera_x, bounds.x1 + center_x), bounds.x2 - center_x);
		camera_y = fmin(fmax(camera_y, bounds.y1 + center_y), bounds.y2 - center_y);
	}

	plx_offset_x = s_frames * s_map->layers[layer].autoscroll_x
		- camera_x * (s_map->layers[layer].parallax_x - 1.0);
	plx_offset_y = s_frames * s_map->layers[layer].autoscroll_y
		- camera_y * (s_map->layers[layer].parallax_y - 1.0);
	x_offset = camera_x - center_x - plx_offset_x;
	y_offset = camera_y - center_y - plx_offset_y;
	if (!s_map->is_repeating && !s_map->layers[layer].is_parallax) {
		if (layer_w < resolution.width)
			x_offset = 0;
		if (layer_h < resolution.height)
			y_offset = 0;
	}
	if (inout_x != NULL)
		*inout_x += x_offset;
	if (inout_y != NULL)
		*inout_y += y_offset;

	if (s_map->is_repeating || s_map->layers[layer].is_parallax) {
		if (inout_x) *inout_x = (*inout_x % layer_w + layer_w) % layer_w;
		if (inout_y) *inout_y = (*inout_y % layer_h + layer_h) % layer_h;
	}
}

Project: minisphere

map_screen_to_map(int camera_x, int camera_y, int* inout_x, int* inout_y)
{
	rect_t  bounds;
	int     center_x;
	int     center_y;
	int     map_h;
	int     map_w;
	size2_t resolution;
	int     tile_h;
	int     tile_w;
	int     x_offset;
	int     y_offset;

	resolution = screen_size(g_screen);
	tileset_get_size(s_map->tileset, &tile_w, &tile_h);
	map_w = s_map->width * tile_w;
	map_h = s_map->height * tile_h;
	center_x = resolution.width / 2;
	center_y = resolution.height / 2;

	if (!s_map->is_repeating) {
		bounds = map_bounds();
		camera_x = fmin(fmax(camera_x, bounds.x1 + center_x), bounds.x2 - center_x);
		camera_y = fmin(fmax(camera_y, bounds.y1 + center_y), bounds.y2 - center_y);
	}

	x_offset = camera_x - center_x;
	y_offset = camera_y - center_y;
	if (!s_map->is_repeating) {
		if (map_w < resolution.width)
			x_offset = 0;
		if (map_h < resolution.height)
			y_offset = 0;
	}
	if (inout_x != NULL)
		*inout_x += x_offset;
	if (inout_y != NULL)
		*inout_y += y_offset;

	if (s_map->is_repeating) {
		if (inout_x) *inout_x = (*inout_x % map_w + map_w) % map_w;
		if (inout_y) *inout_y = (*inout_y % map_h + map_h) % map_h;
	}
}

Project: minisphere

map_set_camera_xy(point2_t where)
{
	s_camera_x = where.x;
	s_camera_y = where.y;
}

Project: minisphere

map_trigger_at(int x, int y, int layer)
{
	rect_t              bounds;
	int                 tile_w, tile_h;
	struct map_trigger* trigger;

	iter_t iter;

	tileset_get_size(s_map->tileset, &tile_w, &tile_h);
	iter = vector_enum(s_map->triggers);
	while ((trigger = iter_next(&iter))) {
		if (trigger->z != layer && false)  // layer ignored for compatibility
			continue;
		bounds.x1 = trigger->x - tile_w / 2;
		bounds.y1 = trigger->y - tile_h / 2;
		bounds.x2 = bounds.x1 + tile_w;
		bounds.y2 = bounds.y1 + tile_h;
		if (is_point_in_rect(x, y, bounds))
			return iter.index;
	}
	return -1;
}

Project: minisphere

map_xy_from_screen(point2_t screen_xy)
{
	int x;
	int y;

	x = screen_xy.x;
	y = screen_xy.y;
	map_screen_to_map(s_camera_x, s_camera_y, &x, &y);
	return mk_point2(x, y);
}

Project: minisphere

map_zone_at(int x, int y, int layer, int which)
{
	struct map_zone* zone;

	iter_t iter;

	iter = vector_enum(s_map->zones);
	while ((zone = iter_next(&iter))) {
		if (zone->layer != layer && false)  // layer ignored for compatibility
			continue;
		if (is_point_in_rect(x, y, zone->bounds) && --which < 0)
			return iter.index;
	}
	return -1;
}

Project: minisphere

person_activate(const person_t* person, person_op_t op, const person_t* acting_person, bool use_default)
{
	const person_t* last_acting;
	const person_t* last_current;

	last_acting = s_acting_person;
	last_current = s_current_person;
	s_acting_person = acting_person;
	s_current_person = person;
	if (use_default)
		script_run(s_def_person_scripts[op], false);
	if (does_person_exist(person))
		script_run(person->scripts[op], false);
	s_acting_person = last_acting;
	s_current_person = last_current;
}

Project: minisphere

person_base(const person_t* person)
{
	rect_t base_rect;
	int    base_x;
	int    base_y;
	double x;
	double y;

	base_rect = rect_zoom(spriteset_get_base(person->sprite), person->scale_x, person->scale_y);
	person_get_xy(person, &x, &y, true);
	base_x = x - (base_rect.x1 + (base_rect.x2 - base_rect.x1) / 2);
	base_y = y - (base_rect.y1 + (base_rect.y2 - base_rect.y1) / 2);
	base_rect.x1 += base_x; base_rect.x2 += base_x;
	base_rect.y1 += base_y; base_rect.y2 += base_y;
	return base_rect;
}

Project: minisphere

person_call_default(const person_t* person, person_op_t op, const person_t* acting_person)
{
	const person_t* last_acting;
	const person_t* last_current;

	last_acting = s_acting_person;
	last_current = s_current_person;
	s_acting_person = acting_person;
	s_current_person = person;
	script_run(s_def_person_scripts[op], false);
	s_acting_person = last_acting;
	s_current_person = last_current;
}

Project: minisphere

person_clear_ignores(person_t* person)
{
	int i;

	for (i = 0; i < person->num_ignores; ++i)
		free(person->ignores[i]);
	person->num_ignores = 0;
}

Project: minisphere

person_clear_queue(person_t* person)
{
	person->num_commands = 0;
}

Project: minisphere

person_compile_script(person_t* person, int type, const lstring_t* codestring)
{
	script_t*   script;
	const char* script_name;

	script_name = type == PERSON_SCRIPT_ON_CREATE ? "onCreate"
		: type == PERSON_SCRIPT_ON_DESTROY ? "onDestroy"
		: type == PERSON_SCRIPT_ON_TOUCH ? "onTouch"
		: type == PERSON_SCRIPT_ON_TALK ? "onTalk"
		: type == PERSON_SCRIPT_GENERATOR ? "genCommands"
		: NULL;
	if (script_name == NULL)
		return false;
	script = script_new(codestring, "%s/%s/%s.js", map_pathname(), person->name, script_name);
	person_on_event(person, type, script);
	return true;
}

Project: minisphere

person_following(const person_t* person, const person_t* leader)
{
	const person_t* node;

	node = person;
	while ((node = node->leader))
		if (node == leader) return true;
	return false;
}

Project: minisphere

person_free(person_t* person)
{
	int i, j;

	person_activate(person, PERSON_SCRIPT_ON_DESTROY, NULL, true);
	for (i = 0; i < s_num_persons; ++i) {
		if (s_persons[i]->leader == person)
			s_persons[i]->leader = NULL;
	}

	detach_person(person);
	for (i = 0; i < s_num_persons; ++i) {
		if (s_persons[i] == person) {
			for (j = i; j < s_num_persons - 1; ++j)
				s_persons[j] = s_persons[j + 1];
			--s_num_persons;
			--i;
		}
	}

	vector_free(person->ignore_list);

	free_person(person);
	sort_persons();
}

Project: minisphere

person_get_color(const person_t* person)
{
	return person->mask;
}

Project: minisphere

person_get_frame(const person_t* person)
{
	int num_frames;

	num_frames = spriteset_num_frames(person->sprite, person->direction);
	return person->frame % num_frames;
}

Project: minisphere

person_get_frame_delay(const person_t* person)
{
	return person->anim_frames;
}

Project: minisphere

person_get_ignore_persons(const person_t* person)
{
	return person->ignore_all_persons;
}

Project: minisphere

person_get_ignore_tiles(const person_t* person)
{
	return person->ignore_all_tiles;
}

Project: minisphere

person_get_layer(const person_t* person)
{
	return person->layer;
}

Project: minisphere

person_get_leader(const person_t* person)
{
	return person->leader;
}

Project: minisphere

person_get_offset(const person_t* person)
{
	return mk_point2(person->x_offset, person->y_offset);
}

Project: minisphere

person_get_pose(const person_t* person)
{
	return person->direction;
}

Project: minisphere

person_get_revert_delay(const person_t* person)
{
	return person->revert_delay;
}

Project: minisphere

person_get_scale(const person_t* person, double* out_scale_x, double* out_scale_y)
{
	*out_scale_x = person->scale_x;
	*out_scale_y = person->scale_y;
}

Project: minisphere

person_get_spriteset(const person_t* person)
{
	return person->sprite;
}

Project: minisphere

person_get_trailing(const person_t* person)
{
	return person->follow_distance;
}

Project: minisphere

person_get_visible(const person_t* person)
{
	return person->is_visible;
}

Project: minisphere

person_get_xy(const person_t* person, double* out_x, double* out_y, bool normalize)
{
	*out_x = person->x;
	*out_y = person->y;
	if (normalize)
		map_normalize_xy(out_x, out_y, person->layer);
}

Project: minisphere

person_get_xyz(const person_t* person, double* out_x, double* out_y, int* out_layer, bool normalize)
{
	*out_x = person->x;
	*out_y = person->y;
	*out_layer = person->layer;
	if (normalize)
		map_normalize_xy(out_x, out_y, *out_layer);
}

Project: minisphere

person_has_moved(const person_t* person)
{
	return person->mv_x != 0 || person->mv_y != 0;
}

Project: minisphere

person_ignore_list(person_t* person)
{

	int i;

	if (person->ignore_list == NULL)
		person->ignore_list = vector_new(sizeof(const char*));
	vector_clear(person->ignore_list);
	for (i = 0; i < person->num_ignores; ++i)
		vector_push(person->ignore_list, &person->ignores[i]);
	return person->ignore_list;
}

Project: minisphere

person_ignore_name(person_t* person, const char* name)
{
	int index;

	index = person->num_ignores++;
	person->ignores = realloc(person->ignores, person->num_ignores * sizeof(char*));
	person->ignores[index] = strdup(name);

	vector_free(person->ignore_list);
	person->ignore_list = NULL;
}

Project: minisphere

person_ignored_by(const person_t* person, const person_t* other)
{

	int i;

	if (other->ignore_all_persons || person->ignore_all_persons)
		return true;
	for (i = 0; i < other->num_ignores; ++i)
		if (strcmp(other->ignores[i], person->name) == 0) return true;
	for (i = 0; i < person->num_ignores; ++i)
		if (strcmp(person->ignores[i], other->name) == 0) return true;
	return false;
}

Project: minisphere

person_moving(const person_t* person)
{
	return person->num_commands > 0;
}

Project: minisphere

person_name(const person_t* person)
{
	return person != NULL ? person->name : "";
}

Project: minisphere

person_new(const char* name, spriteset_t* spriteset, bool is_persistent, script_t* create_script)
{
	point3_t  origin = map_origin();
	person_t* person;

	if (++s_num_persons > s_max_persons) {
		s_max_persons = s_num_persons * 2;
		s_persons = realloc(s_persons, s_max_persons * sizeof(person_t*));
	}
	person = s_persons[s_num_persons - 1] = calloc(1, sizeof(person_t));
	person->id = s_next_person_id++;
	person->sprite = spriteset_ref(spriteset);
	set_person_name(person, name);
	person_set_pose(person, spriteset_pose_name(spriteset, 0));
	person->is_persistent = is_persistent;
	person->is_visible = true;
	person->x = origin.x;
	person->y = origin.y;
	person->layer = origin.z;
	person->speed_x = 1.0;
	person->speed_y = 1.0;
	person->anim_frames = spriteset_frame_delay(person->sprite, person->direction, 0);
	person->mask = mk_color(255, 255, 255, 255);
	person->scale_x = person->scale_y = 1.0;
	person->scripts[PERSON_SCRIPT_ON_CREATE] = create_script;
	person_activate(person, PERSON_SCRIPT_ON_CREATE, NULL, true);
	sort_persons();
	return person;
}

Project: minisphere

person_obstructed_at(const person_t* person, double x, double y, person_t** out_obstructing_person, int* out_tile_index)
{
	rect_t           area;
	rect_t           base, my_base;
	double           cur_x, cur_y;
	bool             is_obstructed = false;
	int              layer;
	const obsmap_t*  obsmap;
	int              tile_w, tile_h;
	const tileset_t* tileset;

	int i, i_x, i_y;

	map_normalize_xy(&x, &y, person->layer);
	person_get_xyz(person, &cur_x, &cur_y, &layer, true);
	my_base = rect_translate(person_base(person), x - cur_x, y - cur_y);
	if (out_obstructing_person != NULL)
		*out_obstructing_person = NULL;
	if (out_tile_index != NULL)
		*out_tile_index = -1;

	if (!person->ignore_all_persons) {
		for (i = 0; i < s_num_persons; ++i) {
			if (s_persons[i] == person)  // these persons aren't going to obstruct themselves!
				continue;
			if (s_persons[i]->layer != layer)
				continue;  // ignore persons not on the same layer
			if (person_following(s_persons[i], person))
				continue;  // ignore own followers
			base = person_base(s_persons[i]);
			if (do_rects_overlap(my_base, base) && !person_ignored_by(person, s_persons[i])) {
				is_obstructed = true;
				if (out_obstructing_person)
					*out_obstructing_person = s_persons[i];
				break;
			}
		}
	}

	obsmap = layer_obsmap(layer);
	if (obsmap_test_rect(obsmap, my_base))
		is_obstructed = true;

	if (!person->ignore_all_tiles) {
		tileset = map_tileset();
		tileset_get_size(tileset, &tile_w, &tile_h);
		area.x1 = my_base.x1 / tile_w;
		area.y1 = my_base.y1 / tile_h;
		area.x2 = area.x1 + (my_base.x2 - my_base.x1) / tile_w + 2;
		area.y2 = area.y1 + (my_base.y2 - my_base.y1) / tile_h + 2;
		for (i_x = area.x1; i_x < area.x2; ++i_x) for (i_y = area.y1; i_y < area.y2; ++i_y) {
			base = rect_translate(my_base, -(i_x * tile_w), -(i_y * tile_h));
			obsmap = tileset_obsmap(tileset, map_tile_at(i_x, i_y, layer));
			if (obsmap != NULL && obsmap_test_rect(obsmap, base)) {
				is_obstructed = true;
				if (out_tile_index)
					*out_tile_index = map_tile_at(i_x, i_y, layer);
				break;
			}
		}
	}

	return is_obstructed;
}

Project: minisphere

person_on_event(person_t* person, int type, script_t* script)
{
	script_unref(person->scripts[type]);
	person->scripts[type] = script;
}

Project: minisphere

person_queue_command(person_t* person, int command, bool is_immediate)
{
	struct command* commands;
	bool            is_aok = true;

	switch (command) {
	case COMMAND_MOVE_NORTHEAST:
		is_aok &= person_queue_command(person, COMMAND_MOVE_NORTH, true);
		is_aok &= person_queue_command(person, COMMAND_MOVE_EAST, is_immediate);
		return is_aok;
	case COMMAND_MOVE_SOUTHEAST:
		is_aok &= person_queue_command(person, COMMAND_MOVE_SOUTH, true);
		is_aok &= person_queue_command(person, COMMAND_MOVE_EAST, is_immediate);
		return is_aok;
	case COMMAND_MOVE_SOUTHWEST:
		is_aok &= person_queue_command(person, COMMAND_MOVE_SOUTH, true);
		is_aok &= person_queue_command(person, COMMAND_MOVE_WEST, is_immediate);
		return is_aok;
	case COMMAND_MOVE_NORTHWEST:
		is_aok &= person_queue_command(person, COMMAND_MOVE_NORTH, true);
		is_aok &= person_queue_command(person, COMMAND_MOVE_WEST, is_immediate);
		return is_aok;
	default:
		++person->num_commands;
		if (person->num_commands > person->max_commands) {
			if (!(commands = realloc(person->commands, person->num_commands * 2 * sizeof(struct command))))
				return false;
			person->max_commands = person->num_commands * 2;
			person->commands = commands;
		}
		person->commands[person->num_commands - 1].type = command;
		person->commands[person->num_commands - 1].is_immediate = is_immediate;
		person->commands[person->num_commands - 1].script = NULL;
		return true;
	}
}

Project: minisphere

person_queue_script(person_t* person, script_t* script, bool is_immediate)
{
	++person->num_commands;
	if (person->num_commands > person->max_commands) {
		person->max_commands = person->num_commands * 2;
		if (!(person->commands = realloc(person->commands, person->max_commands * sizeof(struct command))))
			return false;
	}
	person->commands[person->num_commands - 1].type = COMMAND_RUN_SCRIPT;
	person->commands[person->num_commands - 1].is_immediate = is_immediate;
	person->commands[person->num_commands - 1].script = script;
	return true;
}

Project: minisphere

person_set_angle(person_t* person, double theta)
{
	person->theta = theta;
}

Project: minisphere

person_set_color(person_t* person, color_t mask)
{
	person->mask = mask;
}

Project: minisphere

person_set_frame_delay(person_t* person, int num_frames)
{
	person->anim_frames = num_frames;
	person->revert_frames = person->revert_delay;
}

Project: minisphere

person_set_ignore_tiles (person_t* person, bool ignoring)
{
	person->ignore_all_tiles = ignoring;
}

Project: minisphere

person_set_layer(person_t* person, int layer)
{
	person->layer = layer;
}

Project: minisphere

person_set_leader(person_t* person, person_t* leader, int distance)
{
	const person_t* node;

	if (leader != NULL) {
		node = leader;
		do {
			if (node == person)
				return false;
		} while ((node = node->leader));
	}

	if (leader != NULL) {
		if (!enlarge_step_history(leader, distance))
			return false;
		person->leader = leader;
		person->follow_distance = distance;
	}
	person->leader = leader;
	return true;
}

Project: minisphere

person_set_offset(person_t* person, point2_t offset)
{
	person->x_offset = offset.x;
	person->y_offset = offset.y;
}

Project: minisphere

person_set_pose(person_t* person, const char* pose_name)
{
	person->direction = realloc(person->direction, (strlen(pose_name) + 1) * sizeof(char));
	strcpy(person->direction, pose_name);
}

Project: minisphere

person_set_revert_delay(person_t* person, int num_frames)
{
	person->revert_delay = num_frames;
	person->revert_frames = num_frames;
}

Project: minisphere

person_set_speed(person_t* person, double x_speed, double y_speed)
{
	person->speed_x = x_speed;
	person->speed_y = y_speed;
}

Project: minisphere

person_set_spriteset(person_t* person, spriteset_t* spriteset)
{
	spriteset_t* old_spriteset;

	old_spriteset = person->sprite;
	person->sprite = spriteset_ref(spriteset);
	person->anim_frames = spriteset_frame_delay(person->sprite, person->direction, 0);
	person->frame = 0;
	spriteset_unref(old_spriteset);
}

Project: minisphere

person_set_trailing(person_t* person, int distance)
{
	enlarge_step_history(person->leader, distance);
	person->follow_distance = distance;
}

Project: minisphere

person_set_visible(person_t* person, bool visible)
{
	person->is_visible = visible;
}

Project: minisphere

person_set_xyz(person_t* person, double x, double y, int layer)
{
	person->x = x;
	person->y = y;
	person->layer = layer;
	sort_persons();
}

Project: minisphere

person_talk(const person_t* person)
{
	rect_t          map_rect;
	person_t*       target_person;
	double          talk_x, talk_y;

	map_rect = map_bounds();

	person_get_xy(person, &talk_x, &talk_y, true);
	if (strstr(person->direction, "north"))
		talk_y -= s_talk_distance;
	if (strstr(person->direction, "east"))
		talk_x += s_talk_distance;
	if (strstr(person->direction, "south"))
		talk_y += s_talk_distance;
	if (strstr(person->direction, "west"))
		talk_x -= s_talk_distance;
	person_obstructed_at(person, talk_x, talk_y, &target_person, NULL);

	if (target_person != NULL)
		person_activate(target_person, PERSON_SCRIPT_ON_TALK, person, true);
}

Project: minisphere

process_map_input(void)
{
	int       mv_x, mv_y;
	person_t* person;

	int i;

	kb_clear_queue();

	for (i = 0; i < PLAYER_MAX; ++i) {
		person = s_players[i].person;
		if (person != NULL) {
			if (kb_is_key_down(get_player_key(i, PLAYER_KEY_A))
				|| kb_is_key_down(s_players[i].talk_key)
				|| joy_is_button_down(i, s_talk_button))
			{
				if (s_players[i].is_talk_allowed)
					person_talk(person);
				s_players[i].is_talk_allowed = false;
			}
			else {
				s_players[i].is_talk_allowed = true;
			}
			mv_x = 0; mv_y = 0;
			if (person->num_commands == 0 && person->leader == NULL) {
				if (kb_is_key_down(get_player_key(i, PLAYER_KEY_UP)) || joy_position(i, 1) <= -0.5)
					mv_y = -1;
				if (kb_is_key_down(get_player_key(i, PLAYER_KEY_RIGHT)) || joy_position(i, 0) >= 0.5)
					mv_x = 1;
				if (kb_is_key_down(get_player_key(i, PLAYER_KEY_DOWN)) || joy_position(i, 1) >= 0.5)
					mv_y = 1;
				if (kb_is_key_down(get_player_key(i, PLAYER_KEY_LEFT)) || joy_position(i, 0) <= -0.5)
					mv_x = -1;
			}
			switch (mv_x + mv_y * 3) {
			case -3: // north
				person_queue_command(person, COMMAND_MOVE_NORTH, true);
				person_queue_command(person, COMMAND_FACE_NORTH, true);
				person_queue_command(person, COMMAND_ANIMATE, false);
				break;
			case -2: // northeast
				person_queue_command(person, COMMAND_MOVE_NORTHEAST, true);
				person_queue_command(person, COMMAND_FACE_NORTHEAST, true);
				person_queue_command(person, COMMAND_ANIMATE, false);
				break;
			case 1: // east
				person_queue_command(person, COMMAND_MOVE_EAST, true);
				person_queue_command(person, COMMAND_FACE_EAST, true);
				person_queue_command(person, COMMAND_ANIMATE, false);
				break;
			case 4: // southeast
				person_queue_command(person, COMMAND_MOVE_SOUTHEAST, true);
				person_queue_command(person, COMMAND_FACE_SOUTHEAST, true);
				person_queue_command(person, COMMAND_ANIMATE, false);
				break;
			case 3: // south
				person_queue_command(person, COMMAND_MOVE_SOUTH, true);
				person_queue_command(person, COMMAND_FACE_SOUTH, true);
				person_queue_command(person, COMMAND_ANIMATE, false);
				break;
			case 2: // southwest
				person_queue_command(person, COMMAND_MOVE_SOUTHWEST, true);
				person_queue_command(person, COMMAND_FACE_SOUTHWEST, true);
				person_queue_command(person, COMMAND_ANIMATE, false);
				break;
			case -1: // west
				person_queue_command(person, COMMAND_MOVE_WEST, true);
				person_queue_command(person, COMMAND_FACE_WEST, true);
				person_queue_command(person, COMMAND_ANIMATE, false);
				break;
			case -4: // northwest
				person_queue_command(person, COMMAND_MOVE_NORTHWEST, true);
				person_queue_command(person, COMMAND_FACE_NORTHWEST, true);
				person_queue_command(person, COMMAND_ANIMATE, false);
				break;
			}
		}
	}

	update_bound_keys(true);
}

Project: minisphere

record_step(person_t* person)
{
	struct step* p_step;

	if (person->max_history <= 0)
		return;
	memmove(&person->steps[1], &person->steps[0], (person->max_history - 1) * sizeof(struct step));
	p_step = &person->steps[0];
	p_step->x = person->x;
	p_step->y = person->y;
}

Project: minisphere

reset_persons(bool keep_existing)
{
	unsigned int id;
	point3_t     origin;
	person_t*    person;

	int i, j;

	origin = map_origin();
	for (i = 0; i < s_num_persons; ++i) {
		person = s_persons[i];
		id = person->id;
		if (!keep_existing)
			person->num_commands = 0;
		if (person->is_persistent || keep_existing) {
			person->x = origin.x;
			person->y = origin.y;
			person->layer = origin.z;
		}
		else {
			person_activate(person, PERSON_SCRIPT_ON_DESTROY, NULL, true);
			free_person(person);
			--s_num_persons;
			for (j = i; j < s_num_persons; ++j)
				s_persons[j] = s_persons[j + 1];
			--i;
		}
	}
	sort_persons();
}

Project: minisphere

set_person_name(person_t* person, const char* name)
{
	person->name = realloc(person->name, (strlen(name) + 1) * sizeof(char));
	strcpy(person->name, name);
}

Project: minisphere

sort_persons(void)
{
	qsort(s_persons, s_num_persons, sizeof(person_t*), compare_persons);
}

Project: minisphere

trigger_activate(int trigger_index)
{
	int                 last_trigger;
	struct map_trigger* trigger;

	trigger = vector_get(s_map->triggers, trigger_index);
	last_trigger = s_current_trigger;
	s_current_trigger = trigger_index;
	script_run(trigger->script, true);
	s_current_trigger = last_trigger;
}

Project: minisphere

trigger_get_xyz(int trigger_index, int* out_x, int* out_y, int* out_layer)
{
	struct map_trigger* trigger;

	trigger = vector_get(s_map->triggers, trigger_index);
	if (out_x != NULL)
		*out_x = trigger->x;
	if (out_y != NULL)
		*out_y = trigger->y;
	if (out_layer) *out_layer = trigger->z;
}

Project: minisphere

trigger_set_script(int trigger_index, script_t* script)
{
	script_t*           old_script;
	struct map_trigger* trigger;

	trigger = vector_get(s_map->triggers, trigger_index);
	old_script = trigger->script;
	trigger->script = script_ref(script);
	script_unref(old_script);
}

Project: minisphere

update_map_engine(bool in_main_loop)
{
	bool                has_moved;
	int                 index;
	bool                is_sort_needed = false;
	int                 last_trigger;
	int                 last_zone;
	int                 layer;
	int                 map_w, map_h;
	int                 num_zone_steps;
	script_t*           script_to_run;
	int                 script_type;
	double              start_x[PLAYER_MAX];
	double              start_y[PLAYER_MAX];
	int                 tile_w, tile_h;
	struct map_trigger* trigger;
	double              x, y, px, py;
	struct map_zone*    zone;

	int i, j, k;

	++s_frames;
	tileset_get_size(s_map->tileset, &tile_w, &tile_h);
	map_w = s_map->width * tile_w;
	map_h = s_map->height * tile_h;

	tileset_update(s_map->tileset);

	for (i = 0; i < PLAYER_MAX; ++i) if (s_players[i].person != NULL)
		person_get_xy(s_players[i].person, &start_x[i], &start_y[i], false);
	for (i = 0; i < s_num_persons; ++i) {
		if (s_persons[i]->leader != NULL)
			continue;  // skip followers for now
		update_person(s_persons[i], &has_moved);
		is_sort_needed |= has_moved;
	}
	if (is_sort_needed)
		sort_persons();

	if (s_fade_progress < s_fade_frames) {
		++s_fade_progress;
		s_color_mask = color_mix(s_fade_color_to, s_fade_color_from,
			s_fade_progress, s_fade_frames - s_fade_progress);
	}

	if (s_camera_person != NULL) {
		person_get_xy(s_camera_person, &x, &y, true);
		s_camera_x = x; s_camera_y = y;
	}

	if (in_main_loop && !s_map->is_repeating) {
		script_type = s_camera_y < 0 ? MAP_SCRIPT_ON_LEAVE_NORTH
			: s_camera_x >= map_w ? MAP_SCRIPT_ON_LEAVE_EAST
			: s_camera_y >= map_h ? MAP_SCRIPT_ON_LEAVE_SOUTH
			: s_camera_x < 0 ? MAP_SCRIPT_ON_LEAVE_WEST
			: MAP_SCRIPT_MAX;
		if (script_type < MAP_SCRIPT_MAX)
			map_activate(script_type, true);
	}

	for (i = 0; i < PLAYER_MAX; ++i) if (s_players[i].person != NULL) {
		person_get_xyz(s_players[i].person, &x, &y, &layer, true);
		trigger = get_trigger_at(x, y, layer, &index);
		if (trigger != s_on_trigger) {
			last_trigger = s_current_trigger;
			s_current_trigger = index;
			s_on_trigger = trigger;
			if (trigger != NULL)
				script_run(trigger->script, false);
			s_current_trigger = last_trigger;
		}
	}

	for (k = 0; k < PLAYER_MAX; ++k) if (s_players[k].person != NULL) {
		person_get_xy(s_players[k].person, &x, &y, false);
		px = fabs(x - start_x[k]);
		py = fabs(y - start_y[k]);
		num_zone_steps = px > py ? px : py;
		for (i = 0; i < num_zone_steps; ++i) {
			j = 0;
			while ((zone = get_zone_at(x, y, layer, j++, &index))) {
				if (zone->steps_left-- <= 0) {
					last_zone = s_current_zone;
					s_current_zone = index;
					zone->steps_left = zone->interval;
					script_run(zone->script, true);
					s_current_zone = last_zone;
				}
			}
		}
	}

	for (i = 0; i < s_num_deferreds; ++i) {
		if (s_deferreds[i].frames_left-- <= 0) {
			script_to_run = s_deferreds[i].script;
			for (j = i; j < s_num_deferreds - 1; ++j)
				s_deferreds[j] = s_deferreds[j + 1];
			--s_num_deferreds;
			script_run(script_to_run, false);
			script_unref(script_to_run);
			--i;
		}
	}

	script_run(s_update_script, false);
}

Project: minisphere

update_person(person_t* person, bool* out_has_moved)
{
	struct command  command;
	double          delta_x, delta_y;
	int             facing;
	bool            has_moved;
	bool            is_finished;
	const person_t* last_person;
	struct step     step;
	int             vector;

	int i;

	person->mv_x = 0; person->mv_y = 0;
	if (person->revert_frames > 0 && --person->revert_frames <= 0)
		person->frame = 0;
	if (person->leader == NULL) {  // no leader; use command queue
		if (person->num_commands == 0)
			person_activate(person, PERSON_SCRIPT_GENERATOR, NULL, true);

		is_finished = !does_person_exist(person) || person->num_commands == 0;
		while (!is_finished) {
			command = person->commands[0];
			--person->num_commands;
			for (i = 0; i < person->num_commands; ++i)
				person->commands[i] = person->commands[i + 1];
			last_person = s_current_person;
			s_current_person = person;
			if (command.type != COMMAND_RUN_SCRIPT)
				command_person(person, command.type);
			else
				script_run(command.script, false);
			s_current_person = last_person;
			script_unref(command.script);
			is_finished = !does_person_exist(person)  // stop if person was destroyed
				|| !command.is_immediate || person->num_commands == 0;
		}
	}
	else {  // leader set; follow the leader!
		step = person->leader->steps[person->follow_distance - 1];
		delta_x = step.x - person->x;
		delta_y = step.y - person->y;
		if (fabs(delta_x) > person->speed_x)
			command_person(person, delta_x > 0 ? COMMAND_MOVE_EAST : COMMAND_MOVE_WEST);
		if (!does_person_exist(person)) return;
		if (fabs(delta_y) > person->speed_y)
			command_person(person, delta_y > 0 ? COMMAND_MOVE_SOUTH : COMMAND_MOVE_NORTH);
		if (!does_person_exist(person)) return;
		vector = person->mv_x + person->mv_y * 3;
		facing = vector == -3 ? COMMAND_FACE_NORTH
			: vector == -2 ? COMMAND_FACE_NORTHEAST
			: vector == 1 ? COMMAND_FACE_EAST
			: vector == 4 ? COMMAND_FACE_SOUTHEAST
			: vector == 3 ? COMMAND_FACE_SOUTH
			: vector == 2 ? COMMAND_FACE_SOUTHWEST
			: vector == -1 ? COMMAND_FACE_WEST
			: vector == -4 ? COMMAND_FACE_NORTHWEST
			: COMMAND_WAIT;
		if (facing != COMMAND_WAIT)
			command_person(person, COMMAND_ANIMATE);
		if (!does_person_exist(person)) return;
		command_person(person, facing);
	}

	if (!does_person_exist(person))
		return;  // they probably got eaten by a pig.

	*out_has_moved = person_has_moved(person);
	if (*out_has_moved)
		record_step(person);

	for (i = 0; i < s_num_persons; ++i) {
		if (s_persons[i]->leader != person)
			continue;
		update_person(s_persons[i], &has_moved);
		*out_has_moved |= has_moved;
	}
}

Project: minisphere

zone_activate(int zone_index)
{
	int              last_zone;
	struct map_zone* zone;

	zone = vector_get(s_map->zones, zone_index);
	last_zone = s_current_zone;
	s_current_zone = zone_index;
	script_run(zone->script, true);
	s_current_zone = last_zone;
}

Project: minisphere

zone_get_bounds(int zone_index)
{
	struct map_zone* zone;

	zone = vector_get(s_map->zones, zone_index);
	return zone->bounds;
}

Project: minisphere

zone_get_layer(int zone_index)
{
	struct map_zone* zone;

	zone = vector_get(s_map->zones, zone_index);
	return zone->layer;
}

Project: minisphere

zone_get_steps(int zone_index)
{
	struct map_zone* zone;

	zone = vector_get(s_map->zones, zone_index);
	return zone->interval;
}

Project: minisphere

zone_set_bounds(int zone_index, rect_t bounds)
{
	struct map_zone* zone;

	zone = vector_get(s_map->zones, zone_index);
	rect_normalize(&bounds);
	zone->bounds = bounds;
}

Project: minisphere

zone_set_layer(int zone_index, int layer)
{
	struct map_zone* zone;

	zone = vector_get(s_map->zones, zone_index);
	zone->layer = layer;
}

Project: minisphere

zone_set_script(int zone_index, script_t* script)
{
	script_t*        old_script;
	struct map_zone* zone;

	zone = vector_get(s_map->zones, zone_index);
	old_script = zone->script;
	zone->script = script_ref(script);
	script_unref(old_script);
}

Project: minisphere

zone_set_steps(int zone_index, int interval)
{
	struct map_zone* zone;

	zone = vector_get(s_map->zones, zone_index);
	zone->interval = interval;
	zone->steps_left = 0;
}