/*
 * Portions of this file are copyright Rebirth contributors and licensed as
 * described in COPYING.txt.
 * Portions of this file are copyright Parallax Software and licensed
 * according to the Parallax license below.
 * See COPYING.txt for license details.

THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
*/

/*
 *
 * Code for rendering external scenes
 *
 */

//#define _MARK_ON

#include <algorithm>
#include <stdlib.h>


#include <stdio.h>
#include <string.h>
#include <ctype.h> // for isspace

#include "maths.h"
#include "vecmat.h"
#include "gr.h"
#include "3d.h"
#include "dxxerror.h"
#include "palette.h"
#include "iff.h"
#include "console.h"
#include "texmap.h"
#include "fvi.h"
#include "u_mem.h"
#include "sounds.h"
#include "playsave.h"
#include "inferno.h"
#include "endlevel.h"
#include "object.h"
#include "game.h"
#include "gamepal.h"
#include "screens.h"
#include "terrain.h"
#include "robot.h"
#include "player.h"
#include "physfsx.h"
#include "bm.h"
#include "gameseg.h"
#include "gameseq.h"
#include "newdemo.h"
#include "gamepal.h"
#include "net_udp.h"
#include "vclip.h"
#include "fireball.h"
#include "text.h"
#include "digi.h"
#include "songs.h"
#if defined(DXX_BUILD_DESCENT_II)
#include "movie.h"
#endif
#include "render.h"
#include "hudmsg.h"
#if DXX_USE_OGL
#include "ogl_init.h"
#endif

#include "joy.h"

#if DXX_USE_EDITOR
#include "editor/editor.h"
#endif

#include "compiler-range_for.h"
#include "d_enumerate.h"
#include "d_levelstate.h"
#include "d_range.h"
#include <iterator>

using std::min;
using std::max;

#define SHORT_SEQUENCE	1		//if defined, end sequence when panning starts

namespace dcx {

int Endlevel_sequence;
grs_bitmap *terrain_bitmap;	//!!*exit_bitmap,
polygon_model_index exit_modelnum, destroyed_exit_modelnum;
vms_matrix surface_orient;

namespace {

#define FLY_ACCEL i2f(5)
#define MAX_FLY_OBJECTS 2

d_unique_endlevel_state UniqueEndlevelState;
static void generate_starfield(d_unique_endlevel_state::starfield_type &stars);
static void draw_stars(grs_canvas &, const d_unique_endlevel_state::starfield_type &stars);
static sidenum_t find_exit_side(const d_level_shared_segment_state &LevelSharedSegmentState, const d_level_shared_vertex_state &LevelSharedVertexState, const vms_vector &last_console_player_position, const object_base &obj);

static fix cur_fly_speed, desired_fly_speed;
static grs_bitmap *satellite_bitmap;
vms_vector satellite_pos,satellite_upvec;
static vms_vector station_pos{0xf8c4<<10,0x3c1c<<12,0x372<<10};

static vms_vector mine_exit_point;
static vms_vector mine_ground_exit_point;
static vms_vector mine_side_exit_point;
static vms_matrix mine_exit_orient;
static int outside_mine;

static grs_main_bitmap terrain_bm_instance, satellite_bm_instance;

static int ext_expl_playing,mine_destroyed;
static vms_angvec exit_angles = {-0xa00, 0, 0};
static int endlevel_data_loaded;

static vms_angvec player_angles,player_dest_angles;
#ifndef SHORT_SEQUENCE
static vms_angvec camera_desired_angles,camera_cur_angles;
#endif

static int exit_point_bmx,exit_point_bmy;
static fix satellite_size = i2f(400);

//find delta between two angles
static fixang delta_ang(fixang a,fixang b)
{
	fixang delta0,delta1;
	return (abs(delta0 = a - b) < abs(delta1 = b - a)) ? delta0 : delta1;
}

//return though which side of seg0 is seg1
static sidenum_t matt_find_connect_side(const shared_segment &seg0, const vcsegidx_t seg1)
{
	auto &children = seg0.children;
	return static_cast<sidenum_t>(std::distance(children.begin(), std::find(children.begin(), children.end(), seg1)));
}

static unsigned get_tunnel_length(fvcsegptridx &vcsegptridx, const vcsegptridx_t console_seg, const sidenum_t exit_console_side)
{
	auto seg = console_seg;
	auto exit_side = exit_console_side;
	unsigned tunnel_length = 0;
	for (;;)
	{
		const auto child = seg->shared_segment::children[exit_side];
		if (child == segment_none)
			return 0;
		++tunnel_length;
		if (child == segment_exit)
		{
			assert(seg == PlayerUniqueEndlevelState.exit_segnum);
			return tunnel_length;
		}
		const vcsegidx_t old_segidx = seg;
		seg = vcsegptridx(child);
		const auto entry_side = matt_find_connect_side(seg, old_segidx);
		if (!Side_opposite.valid_index(entry_side))
			return 0;
		exit_side = Side_opposite[entry_side];
	}
}

static vcsegidx_t get_tunnel_transition_segment(const unsigned tunnel_length, const vcsegptridx_t console_seg, const sidenum_t exit_console_side)
{
	auto seg = console_seg;
	auto exit_side = exit_console_side;
	for (auto i = tunnel_length / 3;; --i)
	{
		/*
		 * If the tunnel ended with segment_none, the caller would have
		 * returned immediately after the prior loop.  If the tunnel
		 * ended with segment_exit, then tunnel_length is the count of
		 * segments to reach the exit.  The termination condition on
		 * this loop quits at (tunnel_length / 3), so the loop will quit
		 * before it reaches segment_exit.
		 */
		const auto child = seg->shared_segment::children[exit_side];
		if (!IS_CHILD(child))
			/* This is only a sanity check.  It should never execute
			 * unless there is a bug elsewhere.
			 */
			return seg;
		if (!i)
			return child;
		const vcsegidx_t old_segidx = seg;
		seg = vcsegptridx(child);
		const auto entry_side = matt_find_connect_side(seg, old_segidx);
		exit_side = Side_opposite[entry_side];
	}
}

#define CHASE_TURN_RATE (0x4000/4)		//max turn per second

//returns bitmask of which angles are at dest. bits 0,1,2 = p,b,h
static int chase_angles(vms_angvec *cur_angles,vms_angvec *desired_angles)
{
	vms_angvec delta_angs,alt_angles,alt_delta_angs;
	fix total_delta,alt_total_delta;
	fix frame_turn;
	int mask=0;

	delta_angs.p = desired_angles->p - cur_angles->p;
	delta_angs.h = desired_angles->h - cur_angles->h;
	delta_angs.b = desired_angles->b - cur_angles->b;

	total_delta = abs(delta_angs.p) + abs(delta_angs.b) + abs(delta_angs.h);

	alt_angles.p = f1_0/2 - cur_angles->p;
	alt_angles.b = cur_angles->b + f1_0/2;
	alt_angles.h = cur_angles->h + f1_0/2;

	alt_delta_angs.p = desired_angles->p - alt_angles.p;
	alt_delta_angs.h = desired_angles->h - alt_angles.h;
	alt_delta_angs.b = desired_angles->b - alt_angles.b;

	alt_total_delta = abs(alt_delta_angs.p) + abs(alt_delta_angs.b) + abs(alt_delta_angs.h);

	if (alt_total_delta < total_delta) {
		*cur_angles = alt_angles;
		delta_angs = alt_delta_angs;
	}

	frame_turn = fixmul(FrameTime,CHASE_TURN_RATE);

	if (abs(delta_angs.p) < frame_turn) {
		cur_angles->p = desired_angles->p;
		mask |= 1;
	}
	else
		if (delta_angs.p > 0)
			cur_angles->p += frame_turn;
		else
			cur_angles->p -= frame_turn;

	if (abs(delta_angs.b) < frame_turn) {
		cur_angles->b = desired_angles->b;
		mask |= 2;
	}
	else
		if (delta_angs.b > 0)
			cur_angles->b += frame_turn;
		else
			cur_angles->b -= frame_turn;
//cur_angles->b = 0;

	if (abs(delta_angs.h) < frame_turn) {
		cur_angles->h = desired_angles->h;
		mask |= 4;
	}
	else
		if (delta_angs.h > 0)
			cur_angles->h += frame_turn;
		else
			cur_angles->h -= frame_turn;

	return mask;
}

//find the angle between the player's heading & the station
static vms_angvec get_angs_to_object(const vms_vector &targ_pos, const vms_vector &cur_pos)
{
	return vm_extract_angles_vector(vm_vec_sub(targ_pos, cur_pos));
}

#define MIN_D 0x100

//find which side to fly out of
static sidenum_t find_exit_side(const d_level_shared_segment_state &LevelSharedSegmentState, const d_level_shared_vertex_state &LevelSharedVertexState, const vms_vector &last_console_player_position, const object_base &obj)
{
	auto &Vertices = LevelSharedVertexState.get_vertices();
	vms_vector prefvec;
	fix best_val=-f2_0;

	//find exit side

	vm_vec_normalized_dir_quick(prefvec, obj.pos, last_console_player_position);

	const shared_segment &pseg = LevelSharedSegmentState.get_segments().vcptr(obj.segnum);
	auto &vcvertptr = Vertices.vcptr;
	const auto segcenter{compute_segment_center(vcvertptr, pseg)};

	sidenum_t best_side = side_none;
	for (const auto &&[i, child] : enumerate(pseg.children))
	{
		fix d;

		if (child != segment_none)
		{
			auto sidevec{compute_center_point_on_side(vcvertptr, pseg, i)};
			vm_vec_normalized_dir_quick(sidevec,sidevec,segcenter);
			d = vm_vec_dot(sidevec,prefvec);

			if (labs(d) < MIN_D) d=0;

			if (d > best_val)
			{
				best_val = d;
				best_side = static_cast<sidenum_t>(i);
			}
		}
	}
	return best_side;
}

static void draw_mine_exit_cover(grs_canvas &canvas)
{
	const int of = 10;
	const fix u = i2f(6), d = i2f(9), ur = i2f(14), dr = i2f(17);
	const uint8_t color = BM_XRGB(0, 0, 0);
	g3s_point p0, p1, p2, p3;

	const auto v{vm_vec_scale_add(mine_exit_point, mine_exit_orient.fvec, i2f(of))};

	auto mrd = mine_exit_orient.rvec;
	{
		const auto vu{vm_vec_scale_add(v, mine_exit_orient.uvec, u)};
		auto mru{mrd};
		vm_vec_scale(mru, ur);
		g3_rotate_point(p0, vm_vec_add(vu, mru));
		g3_rotate_point(p1, vm_vec_sub(vu, mru));
	}
	{
		const auto vd{vm_vec_scale_add(v, mine_exit_orient.uvec, -d)};
		vm_vec_scale(mrd, dr);
		g3_rotate_point(p2, vm_vec_sub(vd, mrd));
		g3_rotate_point(p3, vm_vec_add(vd, mrd));
	}
	const std::array<cg3s_point *, 4> pointlist{{
		&p0,
		&p1,
		&p2,
		&p3,
	}};

	g3_draw_poly(canvas, pointlist.size(), pointlist, color);
}

static void generate_starfield(d_unique_endlevel_state::starfield_type &stars)
{
	range_for (auto &i, stars)
	{
		i.x = (d_rand() - D_RAND_MAX / 2) << 14;
		i.z = (d_rand() - D_RAND_MAX / 2) << 14;
		i.y = (d_rand() / 2) << 14;
	}
}

void draw_stars(grs_canvas &canvas, const d_unique_endlevel_state::starfield_type &stars)
{
	int intensity=31;
	g3s_point p;

	uint8_t color = 0;
	for (const auto &&[i, si] : enumerate(stars))
	{
		if ((i&63) == 0) {
			color = BM_XRGB(intensity,intensity,intensity);
			intensity-=3;
		}

		p.p3_vec = g3_rotate_delta_vec(si);
		g3_code_point(p);

		if (p.p3_codes == clipping_code::None)
		{
			p.p3_flags &= ~projection_flag::projected;
			g3_project_point(p);
#if !DXX_USE_OGL
			gr_pixel(canvas.cv_bitmap, f2i(p.p3_sx), f2i(p.p3_sy), color);
#else
			g3_draw_sphere(canvas, p, F1_0 * 3, color);
#endif
		}
	}

//@@	{
//@@		vms_vector delta;
//@@		g3s_point top_pnt;
//@@
//@@		g3_rotate_point(&p,&satellite_pos);
//@@		g3_rotate_delta_vec(&delta,&satellite_upvec);
//@@
//@@		g3_add_delta_vec(&top_pnt,&p,&delta);
//@@
//@@		if (! (p.p3_codes & clipping_code::behind)) {
//@@			int save_im = Interpolation_method;
//@@			Interpolation_method = 0;
//@@			//p.p3_flags &= ~projection_flag::projected;
//@@			g3_project_point(&p);
//@@			if (! (p.p3_flags & projection_flag::overflow))
//@@				//gr_bitmapm(f2i(p.p3_sx)-32,f2i(p.p3_sy)-32,satellite_bitmap);
//@@				g3_draw_rod_tmap(satellite_bitmap,&p,SATELLITE_WIDTH,&top_pnt,SATELLITE_WIDTH,f1_0);
//@@			Interpolation_method = save_im;
//@@		}
//@@	}
}

static void angvec_add2_scale(vms_angvec &dest,const vms_vector &src,fix s)
{
	dest.p += fixmul(src.x,s);
	dest.b += fixmul(src.z,s);
	dest.h += fixmul(src.y,s);
}

static int convert_ext(d_fname &dest, const char (&ext)[4])
{
	auto b = std::begin(dest);
	auto e = std::end(dest);
	auto t = std::find(b, e, '.');
	if (t != e && std::distance(b, t) <= 8)
	{
		std::copy(std::begin(ext), std::end(ext), std::next(t));
		return 1;
	}
	else
		return 0;
}

static std::pair<icsegidx_t, sidenum_t> find_exit_segment_side(fvcsegptridx &vcsegptridx)
{
	range_for (const auto &&segp, vcsegptridx)
	{
		for (const auto &&[sidenum, child_segnum] : enumerate(segp->shared_segment::children))
		{
			if (child_segnum == segment_exit)
			{
				return {segp, static_cast<sidenum_t>(sidenum)};
			}
		}
	}
	return {segment_none, side_none};
}

}

void free_endlevel_data()
{
	terrain_bm_instance.reset();
	satellite_bm_instance.reset();
	free_light_table();
	free_height_array();
}

}

namespace dsx {

namespace {

struct flythrough_data
{
	object		*obj;
	vms_angvec	angles;			//orientation in angles
	vms_vector	step;				//how far in a second
	vms_vector	angstep;			//rotation per second
	fix			speed;			//how fast object is moving
	vms_vector 	headvec;			//where we want to be pointing
	int			first_time;		//flag for if first time through
	fix			offset_frac;	//how far off-center as portion of way
	fix			offset_dist;	//how far currently off-center
};

static std::array<flythrough_data, MAX_FLY_OBJECTS> fly_objects;

//endlevel sequence states

#define EL_OFF				0		//not in endlevel
#define EL_FLYTHROUGH	1		//auto-flythrough in tunnel
#define EL_LOOKBACK		2		//looking back at player
#define EL_OUTSIDE		3		//flying outside for a while
#define EL_STOPPED		4		//stopped, watching explosion
#define EL_PANNING		5		//panning around, watching player
#define EL_CHASING		6		//chasing player to station

static object *endlevel_camera;
static object *external_explosion;

#define FLY_SPEED i2f(50)

static void do_endlevel_flythrough(d_level_unique_object_state &LevelUniqueObjectState, const d_level_shared_segment_state &LevelSharedSegmentState, d_level_unique_segment_state &LevelUniqueSegmentState, flythrough_data *flydata);

#define DEFAULT_SPEED i2f(16)

#if defined(DXX_BUILD_DESCENT_II)
constexpr std::array<const char, 24> movie_table{{
	'A','B','C','A',
	'D','F','D','F',
	'G','H','I','G',
	'J','K','L','J',
	'M','O','M','O',
	'P','Q','P','Q'
}};
static auto endlevel_movie_played = movie_play_status::skipped;

//returns movie played status.  see movie.h
static movie_play_status start_endlevel_movie()
{
	palette_array_t save_pal;

	//Assert(PLAYING_BUILTIN_MISSION); //only play movie for built-in mission

	const auto current_level_num = Current_level_num;
	if (is_SHAREWARE)
		return movie_play_status::skipped;
	if (!is_D2_OEM)
		if (current_level_num == Current_mission->last_level)
			return movie_play_status::started;   //don't play movie

	if (!(current_level_num > 0))
		return movie_play_status::skipped;       //no escapes for secret level
	char movie_name[] = "ESA.MVE";
	movie_name[2] = movie_table[Current_level_num-1];

	save_pal = gr_palette;

	const auto r = PlayMovie({}, movie_name, (Game_mode & GM_MULTI) ? play_movie_warn_missing::verbose : play_movie_warn_missing::urgent);
	if (Newdemo_state == ND_STATE_PLAYBACK) {
		set_screen_mode(SCREEN_GAME);
		gr_palette = save_pal;
	}
	return (r);
}
#endif

//if speed is zero, use default speed
void start_endlevel_flythrough(flythrough_data &flydata, object &obj, const fix speed)
{
	flydata.obj = &obj;
	flydata.first_time = 1;
	flydata.speed = speed?speed:DEFAULT_SPEED;
	flydata.offset_frac = 0;
}

void draw_exit_model(grs_canvas &canvas)
{
	int f=15,u=0;	//21;
	g3s_lrgb lrgb = { f1_0, f1_0, f1_0 };

	if (mine_destroyed)
	{
		// draw black shape to mask out terrain in exit hatch
		draw_mine_exit_cover(canvas);
	}

	auto model_pos{vm_vec_scale_add(mine_exit_point, mine_exit_orient.fvec, i2f(f))};
	vm_vec_scale_add2(model_pos,mine_exit_orient.uvec,i2f(u));
	auto &Polygon_models = LevelSharedPolygonModelState.Polygon_models;
	draw_polygon_model(Polygon_models, canvas, draw_tmap, model_pos, mine_exit_orient, nullptr, mine_destroyed ? destroyed_exit_modelnum : exit_modelnum, 0, lrgb, nullptr, alternate_textures{});
}

#define SATELLITE_DIST		i2f(1024)
#define SATELLITE_WIDTH		satellite_size
#define SATELLITE_HEIGHT	((satellite_size*9)/4)		//((satellite_size*5)/2)

static void render_external_scene(fvcobjptridx &vcobjptridx, grs_canvas &canvas, const d_level_unique_light_state &LevelUniqueLightState, const fix eye_offset)
{
	auto &Objects = LevelUniqueObjectState.Objects;
	auto &vmobjptridx = Objects.vmptridx;
#if DXX_USE_OGL
	int orig_Render_depth = Render_depth;
#endif
	g3s_lrgb lrgb = { f1_0, f1_0, f1_0 };

	auto Viewer_eye = Viewer->pos;

	if (eye_offset)
		vm_vec_scale_add2(Viewer_eye,Viewer->orient.rvec,eye_offset);

	g3_set_view_matrix(Viewer->pos,Viewer->orient,Render_zoom);

	//g3_draw_horizon(BM_XRGB(0,0,0),BM_XRGB(16,16,16));		//,-1);
	gr_clear_canvas(canvas, BM_XRGB(0,0,0));

	{
		auto &&ctx = g3_start_instance_matrix(vmd_zero_vector, surface_orient);
		draw_stars(canvas, UniqueEndlevelState.stars);
		g3_done_instance(ctx);
	}

	{	//draw satellite
		g3s_point top_pnt;

		const auto p = g3_rotate_point(satellite_pos);
		const auto delta{g3_rotate_delta_vec(satellite_upvec)};

		g3_add_delta_vec(top_pnt,p,delta);

		if ((p.p3_codes & clipping_code::behind) == clipping_code::None)
		{
			//p.p3_flags &= ~projection_flag::projected;
			//g3_project_point(&p);
			if (! (p.p3_flags & projection_flag::overflow)) {
				push_interpolation_method save_im(0);
				//gr_bitmapm(f2i(p.p3_sx)-32,f2i(p.p3_sy)-32,satellite_bitmap);
				g3_draw_rod_tmap(canvas, *satellite_bitmap, p, SATELLITE_WIDTH, top_pnt, SATELLITE_WIDTH, lrgb, draw_tmap);
			}
		}
	}

#if DXX_USE_OGL
	ogl_toggle_depth_test(0);
	Render_depth = (200-(vm_vec_dist_quick(mine_ground_exit_point, Viewer_eye)/F1_0))/36;
#endif
	render_terrain(canvas, Viewer_eye, mine_ground_exit_point, exit_point_bmx, exit_point_bmy);
#if DXX_USE_OGL
	Render_depth = orig_Render_depth;
	ogl_toggle_depth_test(1);
#endif

	draw_exit_model(canvas);
	if (ext_expl_playing)
	{
		const auto alpha = PlayerCfg.AlphaBlendMineExplosion;
		if (alpha) // set nice transparency/blending for the big explosion
			gr_settransblend(canvas, GR_FADE_OFF, gr_blend::additive_c);
		draw_fireball(Vclip, canvas, vcobjptridx(external_explosion));
#if DXX_USE_OGL
		/* If !OGL, the third argument is discarded, so this call
		 * becomes the same as the one above.
		 */
		if (alpha)
			gr_settransblend(canvas, GR_FADE_OFF, gr_blend::normal); // revert any transparency/blending setting back to normal
#endif
	}

#if !DXX_USE_OGL
	Lighting_on=0;
#endif
	render_object(canvas, LevelUniqueLightState, vmobjptridx(ConsoleObject));
#if !DXX_USE_OGL
	Lighting_on=1;
#endif
}

}

window_event_result start_endlevel_sequence()
{
	auto &Objects = LevelUniqueObjectState.Objects;
	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
	auto &vmobjptr = Objects.vmptr;
	const auto dead = Player_dead_state != player_dead_state::no || (ConsoleObject->flags & OF_SHOULD_BE_DEAD);
	if (!dead)
		reset_rear_view(); //turn off rear view if set - NOTE: make sure this happens before we pause demo recording!!

	if (Newdemo_state == ND_STATE_RECORDING)		// stop demo recording
		Newdemo_state = ND_STATE_PAUSED;

	if (Newdemo_state == ND_STATE_PLAYBACK) {		// don't do this if in playback mode
#if defined(DXX_BUILD_DESCENT_II)
		if (PLAYING_BUILTIN_MISSION) // only play movie for built-in mission
		{
			const auto g = Game_wind;
			g->set_visible(0);	// suspend the game, including drawing
			start_endlevel_movie();
			g->set_visible(1);
		}
		strcpy(last_palette_loaded,"");		//force palette load next time
#endif
		return window_event_result::ignored;
	}

	if (dead)
		return window_event_result::ignored;				//don't start if dead!
	con_puts(CON_NORMAL, "You have escaped the mine!");

#if defined(DXX_BUILD_DESCENT_II)
	auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
	//	Dematerialize Buddy!
	for (auto &obj : vmobjptr)
	{
		if (obj.type == OBJ_ROBOT)
			if (Robot_info[get_robot_id(obj)].companion) {
				object_create_explosion_without_damage(Vclip, vmsegptridx(obj.segnum), obj.pos, F1_0 * 7 / 2, vclip_index::powerup_disappearance);
				obj.flags |= OF_SHOULD_BE_DEAD;
			}
	}
#endif

	get_local_plrobj().ctype.player_info.homing_object_dist = -F1_0; // Turn off homing sound.

	if (Game_mode & GM_MULTI) {
		multi_send_endlevel_start(multi_endlevel_type::normal);
		multi::dispatch->do_protocol_frame(1, 1);
	}

#if defined(DXX_BUILD_DESCENT_I)
	if (!endlevel_data_loaded)
#elif defined(DXX_BUILD_DESCENT_II)

	if (PLAYING_BUILTIN_MISSION) // only play movie for built-in mission
		if (!(Game_mode & GM_MULTI))
		{
			const auto g = Game_wind;
			g->set_visible(0);	// suspend the game, including drawing
			endlevel_movie_played = start_endlevel_movie();
			g->set_visible(1);
		}

	if (!(!(Game_mode & GM_MULTI) && (endlevel_movie_played == movie_play_status::skipped) && endlevel_data_loaded))
#endif
	{
		return PlayerFinishedLevel(next_level_request_secret_flag::only_normal_level);		//done with level
	}
#if defined(DXX_BUILD_DESCENT_II)
	int exit_models_loaded = 0;

	if (Piggy_hamfile_version < pig_hamfile_version::_3)
		exit_models_loaded = 1; // built-in for PC shareware

	else
		exit_models_loaded = load_exit_models();

	if (!exit_models_loaded)
		return window_event_result::ignored;
#endif
	{
		//count segments in exit tunnel

		const object_base &console = vmobjptr(ConsoleObject);
		const auto exit_console_side = find_exit_side(LevelSharedSegmentState, LevelSharedVertexState, LevelUniqueObjectState.last_console_player_position, console);
		const auto &&console_seg = vcsegptridx(console.segnum);
		const auto tunnel_length = get_tunnel_length(vcsegptridx, console_seg, exit_console_side);
		if (!tunnel_length)
		{
			return PlayerFinishedLevel(next_level_request_secret_flag::only_normal_level);		//don't do special sequence
		}
		//now pick transition segnum 1/3 of the way in

		PlayerUniqueEndlevelState.transition_segnum = get_tunnel_transition_segment(tunnel_length, console_seg, exit_console_side);
	}

	if (Game_mode & GM_MULTI) {
		multi_send_endlevel_start(multi_endlevel_type::normal);
		multi::dispatch->do_protocol_frame(1, 1);
	}
	songs_play_song(song_number::endlevel, 0);

	Endlevel_sequence = EL_FLYTHROUGH;

	ConsoleObject->movement_source = object::movement_type::None;			//movement handled by flythrough
	ConsoleObject->control_source = object::control_type::None;

	Game_suspended |= SUSP_ROBOTS;          //robots don't move

	cur_fly_speed = desired_fly_speed = FLY_SPEED;

	start_endlevel_flythrough(fly_objects[0], vmobjptr(ConsoleObject), cur_fly_speed);		//initialize

	{
		const auto &&m = TXT_EXIT_SEQUENCE;
		HUD_init_message_literal(HM_DEFAULT, {m, strlen(m)});
	}

	outside_mine = ext_expl_playing = 0;

	flash_scale = f1_0;

	generate_starfield(UniqueEndlevelState.stars);

	mine_destroyed=0;

	return window_event_result::handled;
}

window_event_result stop_endlevel_sequence()
{
#if !DXX_USE_OGL
	Interpolation_method = 0;
#endif

	select_cockpit(PlayerCfg.CockpitMode[0]);

	Endlevel_sequence = EL_OFF;
	return PlayerFinishedLevel(next_level_request_secret_flag::only_normal_level);
}

window_event_result do_endlevel_frame(const d_level_shared_robot_info_state &LevelSharedRobotInfoState)
{
	auto &Objects = LevelUniqueObjectState.Objects;
	auto &vmobjptr = Objects.vmptr;
	static fix timer;
	static fix bank_rate;
	static fix explosion_wait1=0;
	static fix explosion_wait2=0;
	static fix ext_expl_halflife;

	auto result = endlevel_move_all_objects(LevelSharedRobotInfoState);

	if (ext_expl_playing) {

		do_explosion_sequence(LevelSharedRobotInfoState.Robot_info, vmobjptr(external_explosion));

		if (external_explosion->lifeleft < ext_expl_halflife)
			mine_destroyed = 1;

		if (external_explosion->flags & OF_SHOULD_BE_DEAD)
			ext_expl_playing = 0;
	}

	if (cur_fly_speed != desired_fly_speed) {
		fix delta = desired_fly_speed - cur_fly_speed;
		fix frame_accel = fixmul(FrameTime,FLY_ACCEL);

		if (abs(delta) < frame_accel)
			cur_fly_speed = desired_fly_speed;
		else
			if (delta > 0)
				cur_fly_speed += frame_accel;
			else
				cur_fly_speed -= frame_accel;
	}

	//do big explosions
	if (!outside_mine) {

		if (Endlevel_sequence==EL_OUTSIDE) {
			const auto tvec{vm_vec_sub(ConsoleObject->pos, mine_side_exit_point)};
			if (vm_vec_dot(tvec,mine_exit_orient.fvec) > 0) {
				vms_vector mov_vec;

				outside_mine = 1;

				const auto &&exit_segp = vmsegptridx(PlayerUniqueEndlevelState.exit_segnum);
				const auto &&tobj = object_create_explosion_without_damage(Vclip, exit_segp, mine_side_exit_point, i2f(50), vclip_index::big_player_explosion);

				if (tobj) {
				// Move explosion to Viewer to draw it in front of mine exit model
				vm_vec_normalized_dir_quick(mov_vec,Viewer->pos,tobj->pos);
				vm_vec_scale_add2(tobj->pos,mov_vec,i2f(30));
					external_explosion = tobj;

					flash_scale = 0;	//kill lights in mine

					ext_expl_halflife = tobj->lifeleft;

					ext_expl_playing = 1;
				}

				digi_link_sound_to_pos(SOUND_BIG_ENDLEVEL_EXPLOSION, exit_segp, sidenum_t::WLEFT, mine_side_exit_point, 0, i2f(3)/4);
			}
		}

		//do explosions chasing player
		if ((explosion_wait1-=FrameTime) < 0) {
			static int sound_count;

			auto tpnt{vm_vec_scale_add(ConsoleObject->pos, ConsoleObject->orient.fvec, -ConsoleObject->size * 5)};
			vm_vec_scale_add2(tpnt,ConsoleObject->orient.rvec,(d_rand()-D_RAND_MAX/2)*15);
			vm_vec_scale_add2(tpnt,ConsoleObject->orient.uvec,(d_rand()-D_RAND_MAX/2)*15);

			const auto &&segnum = find_point_seg(LevelSharedSegmentState, LevelUniqueSegmentState, tpnt, Segments.vmptridx(ConsoleObject->segnum));

			if (segnum != segment_none) {
				object_create_explosion_without_damage(Vclip, segnum, tpnt, i2f(20), vclip_index::big_player_explosion);
			       	if (d_rand()<10000 || ++sound_count==7) {		//pseudo-random
					digi_link_sound_to_pos( SOUND_TUNNEL_EXPLOSION, segnum, sidenum_t::WLEFT, tpnt, 0, F1_0 );
					sound_count=0;
				}
			}

			explosion_wait1 = 0x2000 + d_rand()/4;

		}
	}

	//do little explosions on walls
	if (Endlevel_sequence >= EL_FLYTHROUGH && Endlevel_sequence < EL_OUTSIDE)
		if ((explosion_wait2-=FrameTime) < 0) {
			fvi_info hit_data;

			//create little explosion on wall

			auto tpnt{vm_vec_copy_scale(ConsoleObject->orient.rvec, (d_rand() - D_RAND_MAX / 2) * 100)};
			vm_vec_scale_add2(tpnt,ConsoleObject->orient.uvec,(d_rand()-D_RAND_MAX/2)*100);
			vm_vec_add2(tpnt,ConsoleObject->pos);

			if (Endlevel_sequence == EL_FLYTHROUGH)
				vm_vec_scale_add2(tpnt,ConsoleObject->orient.fvec,d_rand()*200);
			else
				vm_vec_scale_add2(tpnt,ConsoleObject->orient.fvec,d_rand()*60);

			//find hit point on wall

			const auto hit_type = find_vector_intersection(fvi_query{
				ConsoleObject->pos,
				tpnt,
				fvi_query::unused_ignore_obj_list,
				fvi_query::unused_LevelUniqueObjectState,
				fvi_query::unused_Robot_info,
				0,
				Objects.icptridx(object_first),
			}, ConsoleObject->segnum, 0, hit_data);

			if (hit_type == fvi_hit_type::Wall && hit_data.hit_seg != segment_none)
				object_create_explosion_without_damage(Vclip, vmsegptridx(hit_data.hit_seg), hit_data.hit_pnt, i2f(3) + d_rand() * 6, vclip_index::small_explosion);

			explosion_wait2 = (0xa00 + d_rand()/8)/2;
		}

	switch (Endlevel_sequence) {

		case EL_OFF: return result;

		case EL_FLYTHROUGH: {

			do_endlevel_flythrough(LevelUniqueObjectState, LevelSharedSegmentState, LevelUniqueSegmentState, &fly_objects[0]);

			if (ConsoleObject->segnum == PlayerUniqueEndlevelState.transition_segnum)
			{
#if defined(DXX_BUILD_DESCENT_II)
				if (PLAYING_BUILTIN_MISSION && endlevel_movie_played != movie_play_status::skipped)
					result = std::max(stop_endlevel_sequence(), result);
				else
#endif
				{

					//songs_play_song(song_number::endlevel, 0);

					Endlevel_sequence = EL_LOOKBACK;

					const auto &&objnum = obj_create(LevelUniqueObjectState, LevelSharedSegmentState, LevelUniqueSegmentState, OBJ_CAMERA, 0,
					                    vmsegptridx(ConsoleObject->segnum), ConsoleObject->pos, &ConsoleObject->orient, 0, 
					                    object::control_type::None, object::movement_type::None, render_type::RT_NONE);

					if (objnum == object_none) { //can't get object, so abort
						return std::max(stop_endlevel_sequence(), result);
					}

					Viewer = endlevel_camera = objnum;

					select_cockpit(cockpit_mode_t::letterbox);

					fly_objects[1] = fly_objects[0];
					fly_objects[1].obj = endlevel_camera;
					fly_objects[1].speed = (5*cur_fly_speed)/4;
					fly_objects[1].offset_frac = 0x4000;

					vm_vec_scale_add2(endlevel_camera->pos,endlevel_camera->orient.fvec,i2f(7));

					timer=0x20000;

				}
			}

			break;
		}


		case EL_LOOKBACK: {

			do_endlevel_flythrough(LevelUniqueObjectState, LevelSharedSegmentState, LevelUniqueSegmentState, &fly_objects[0]);
			do_endlevel_flythrough(LevelUniqueObjectState, LevelSharedSegmentState, LevelUniqueSegmentState, &fly_objects[1]);

			if (timer>0) {

				timer -= FrameTime;

				if (timer < 0)		//reduce speed
					fly_objects[1].speed = fly_objects[0].speed;
			}

			if (endlevel_camera->segnum == PlayerUniqueEndlevelState.exit_segnum)
			{
				Endlevel_sequence = EL_OUTSIDE;

				timer = i2f(2);

				vm_vec_negate(endlevel_camera->orient.fvec);
				vm_vec_negate(endlevel_camera->orient.rvec);

				const auto cam_angles{vm_extract_angles_matrix(endlevel_camera->orient)};
				const auto exit_seg_angles{vm_extract_angles_matrix(mine_exit_orient)};
				bank_rate = (-exit_seg_angles.b - cam_angles.b)/2;

				ConsoleObject->control_source = endlevel_camera->control_source = object::control_type::None;

			}

			break;
		}

		case EL_OUTSIDE: {
			vm_vec_scale_add2(ConsoleObject->pos,ConsoleObject->orient.fvec,fixmul(FrameTime,cur_fly_speed));
			vm_vec_scale_add2(endlevel_camera->pos,endlevel_camera->orient.fvec,fixmul(FrameTime,-2*cur_fly_speed));
			vm_vec_scale_add2(endlevel_camera->pos,endlevel_camera->orient.uvec,fixmul(FrameTime,-cur_fly_speed/10));

			auto cam_angles{vm_extract_angles_matrix(endlevel_camera->orient)};
			cam_angles.b += fixmul(bank_rate,FrameTime);
			vm_angles_2_matrix(endlevel_camera->orient,cam_angles);

			timer -= FrameTime;

			if (timer < 0) {

				Endlevel_sequence = EL_STOPPED;

				player_angles = vm_extract_angles_matrix(ConsoleObject->orient);

				timer = i2f(3);

			}

			break;
		}

		case EL_STOPPED: {

			player_dest_angles = get_angs_to_object(station_pos, ConsoleObject->pos);
			chase_angles(&player_angles,&player_dest_angles);
			vm_angles_2_matrix(ConsoleObject->orient,player_angles);

			vm_vec_scale_add2(ConsoleObject->pos,ConsoleObject->orient.fvec,fixmul(FrameTime,cur_fly_speed));

			timer -= FrameTime;

			if (timer < 0) {


				#ifdef SHORT_SEQUENCE

				result = std::max(stop_endlevel_sequence(), result);

				#else
				Endlevel_sequence = EL_PANNING;

				camera_cur_angles = vm_extract_angles_matrix(endlevel_camera->orient);


				timer = i2f(3);

				if (Game_mode & GM_MULTI) { // try to skip part of the seq if multiplayer
					result = std::max(stop_endlevel_sequence(), result);
					return result;
				}

				#endif		//SHORT_SEQUENCE

			}
			break;
		}

		#ifndef SHORT_SEQUENCE
		case EL_PANNING: {
			int mask;

			player_dest_angles = get_angs_to_object(station_pos, ConsoleObject->pos);
			chase_angles(&player_angles,&player_dest_angles);
			vm_angles_2_matrix(ConsoleObject->orient,player_angles);
			vm_vec_scale_add2(ConsoleObject->pos,ConsoleObject->orient.fvec,fixmul(FrameTime,cur_fly_speed));


			camera_desired_angles = get_angs_to_object(ConsoleObject->pos, endlevel_camera->pos);
			mask = chase_angles(&camera_cur_angles,&camera_desired_angles);
			vm_angles_2_matrix(endlevel_camera->orient,camera_cur_angles);

			if ((mask&5) == 5) {

				vms_vector tvec;

				Endlevel_sequence = EL_CHASING;

				vm_vec_normalized_dir_quick(tvec,station_pos,ConsoleObject->pos);
				vm_vector_to_matrix_u(ConsoleObject->orient, tvec, surface_orient.uvec);

				desired_fly_speed *= 2;
			}

			break;
		}

		case EL_CHASING: {
			fix d,speed_scale;


			camera_desired_angles = get_angs_to_object(ConsoleObject->pos, endlevel_camera->pos);
			chase_angles(&camera_cur_angles,&camera_desired_angles);

			vm_angles_2_matrix(endlevel_camera->orient,camera_cur_angles);

			d = vm_vec_dist_quick(ConsoleObject->pos,endlevel_camera->pos);

			speed_scale = fixdiv(d,i2f(0x20));
			if (d<f1_0) d=f1_0;

			player_dest_angles = get_angs_to_object(station_pos, ConsoleObject->pos);
			chase_angles(&player_angles,&player_dest_angles);
			vm_angles_2_matrix(ConsoleObject->orient,player_angles);

			vm_vec_scale_add2(ConsoleObject->pos,ConsoleObject->orient.fvec,fixmul(FrameTime,cur_fly_speed));
			vm_vec_scale_add2(endlevel_camera->pos,endlevel_camera->orient.fvec,fixmul(FrameTime,fixmul(speed_scale,cur_fly_speed)));

			if (vm_vec_dist(ConsoleObject->pos,station_pos) < i2f(10))
				result = std::max(stop_endlevel_sequence(), result);

			break;

		}
		#endif		//ifdef SHORT_SEQUENCE

	}

	return result;
}

namespace {

static void endlevel_render_mine(const d_level_shared_segment_state &LevelSharedSegmentState, grs_canvas &canvas, fix eye_offset)
{
	auto Viewer_eye = Viewer->pos;

	if (Viewer->type == OBJ_PLAYER )
		vm_vec_scale_add2(Viewer_eye,Viewer->orient.fvec,(Viewer->size*3)/4);

	if (eye_offset)
		vm_vec_scale_add2(Viewer_eye,Viewer->orient.rvec,eye_offset);

#if DXX_USE_EDITOR
	if (EditorWindow)
		Viewer_eye = Viewer->pos;
	#endif

	segnum_t start_seg_num;
	if (Endlevel_sequence >= EL_OUTSIDE) {
		start_seg_num = PlayerUniqueEndlevelState.exit_segnum;
	}
	else {
		start_seg_num = find_point_seg(LevelSharedSegmentState, Viewer_eye, Segments.vcptridx(Viewer->segnum));

		if (start_seg_num==segment_none)
			start_seg_num = Viewer->segnum;
	}

	g3_set_view_matrix(Viewer_eye, Endlevel_sequence == EL_LOOKBACK
		? vm_matrix_x_matrix(Viewer->orient, vm_angles_2_matrix(vms_angvec{0, 0, INT16_MAX}))
		: Viewer->orient, Render_zoom);

	window_rendered_data window;
	render_mine(canvas, Viewer_eye, start_seg_num, eye_offset, window);
}

}

void render_endlevel_frame(grs_canvas &canvas, fix eye_offset)
{
	auto &Objects = LevelUniqueObjectState.Objects;
	auto &vcobjptridx = Objects.vcptridx;
	g3_start_frame(canvas);

	if (Endlevel_sequence < EL_OUTSIDE)
		endlevel_render_mine(LevelSharedSegmentState, canvas, eye_offset);
	else
		render_external_scene(vcobjptridx, canvas, LevelUniqueLightState, eye_offset);

	g3_end_frame();
}

///////////////////////// copy of flythrough code for endlevel

#define MAX_ANGSTEP	0x4000		//max turn per second

#define MAX_SLIDE_PER_SEGMENT 0x10000

namespace {

void do_endlevel_flythrough(d_level_unique_object_state &LevelUniqueObjectState, const d_level_shared_segment_state &LevelSharedSegmentState, d_level_unique_segment_state &LevelUniqueSegmentState, flythrough_data *const flydata)
{
	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
	auto &Objects = LevelUniqueObjectState.Objects;
	auto &Vertices = LevelSharedVertexState.get_vertices();
	const auto &&obj = Objects.vmptridx(flydata->obj);
	
	vcsegidx_t old_player_seg = obj->segnum;

	//move the player for this frame

	if (!flydata->first_time) {

		vm_vec_scale_add2(obj->pos,flydata->step,FrameTime);
		angvec_add2_scale(flydata->angles,flydata->angstep,FrameTime);

		vm_angles_2_matrix(obj->orient,flydata->angles);
	}

	//check new player seg

	update_object_seg(Objects.vmptr, LevelSharedSegmentState, LevelUniqueSegmentState, obj);
	const shared_segment &pseg = *vcsegptr(obj->segnum);

	if (flydata->first_time || obj->segnum != old_player_seg) {		//moved into new seg
		fix seg_time;
		sidenum_t up_side{};

		sidenum_t exit_side;	//what side we leave through
		sidenum_t entry_side{};	//what side we enter through

		//find new exit side

		if (flydata->first_time ||
			(entry_side = matt_find_connect_side(vcsegptr(obj->segnum), old_player_seg)) == side_none ||
			pseg.children[exit_side = Side_opposite[entry_side]] == segment_none)
			exit_side = find_exit_side(LevelSharedSegmentState, LevelSharedVertexState, LevelUniqueObjectState.last_console_player_position, obj);

		{										//find closest side to align to
			fix d,largest_d=-f1_0;
			for (const auto i : MAX_SIDES_PER_SEGMENT)
			{
				d = vm_vec_dot(pseg.sides[i].normals[0], flydata->obj->orient.uvec);
				if (d > largest_d) {largest_d = d; up_side=i;}
			}
		}

		//update target point & angles

		//where we are heading (center of exit_side)
		auto &vcvertptr = Vertices.vcptr;
		auto dest_point{compute_center_point_on_side(vcvertptr, pseg, exit_side)};
		const vms_vector nextcenter{(pseg.children[exit_side] == segment_exit)
			? dest_point
			: compute_segment_center(vcvertptr, vcsegptr(pseg.children[exit_side]))
		};

		//update target point and movement points

		//offset object sideways
		if (flydata->offset_frac) {
			std::array<vms_vector, 2> sp;
			auto isp = sp.begin();
			for (const auto i : MAX_SIDES_PER_SEGMENT)
				if (i!=entry_side && i!=exit_side && i!=up_side && i!=Side_opposite[up_side])
				 {
					 *isp = compute_center_point_on_side(vcvertptr, pseg, i);
					 ++ isp;
					 if (isp == sp.end())
						 break;
				 }

			const fix dist = std::min(fixmul(vm_vec_dist(sp[0], sp[1]), flydata->offset_frac), flydata->offset_dist + MAX_SLIDE_PER_SEGMENT);
			flydata->offset_dist = dist;

			vm_vec_scale_add2(dest_point,obj->orient.rvec,dist);
		}

		vm_vec_sub(flydata->step,dest_point,obj->pos);
		auto step_size = vm_vec_normalize_quick(flydata->step);
		vm_vec_scale(flydata->step,flydata->speed);

		const auto curcenter{compute_segment_center(vcvertptr, pseg)};
		vm_vec_sub(flydata->headvec,nextcenter,curcenter);

		const auto dest_orient{vm_vector_to_matrix_u(flydata->headvec, pseg.sides[up_side].normals[0])};
		//where we want to be pointing
		const auto dest_angles{vm_extract_angles_matrix(dest_orient)};

		if (flydata->first_time)
			flydata->angles = vm_extract_angles_matrix(obj->orient);

		seg_time = fixdiv(step_size,flydata->speed);	//how long through seg

		if (seg_time) {
			flydata->angstep.x = max(-MAX_ANGSTEP,min(MAX_ANGSTEP,fixdiv(delta_ang(flydata->angles.p,dest_angles.p),seg_time)));
			flydata->angstep.z = max(-MAX_ANGSTEP,min(MAX_ANGSTEP,fixdiv(delta_ang(flydata->angles.b,dest_angles.b),seg_time)));
			flydata->angstep.y = max(-MAX_ANGSTEP,min(MAX_ANGSTEP,fixdiv(delta_ang(flydata->angles.h,dest_angles.h),seg_time)));

		}
		else {
			flydata->angles = dest_angles;
			flydata->angstep.x = flydata->angstep.y = flydata->angstep.z = 0;
		}
	}

	flydata->first_time=0;
}

}

#define LINE_LEN	80
#define NUM_VARS	8

#define STATION_DIST	i2f(1024)

//called for each level to load & setup the exit sequence

void load_endlevel_data(int level_num)
{
	auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
	auto &Vertices = LevelSharedVertexState.get_vertices();
	d_fname filename;
	char *p;
	int var;
	int have_binary = 0;

	endlevel_data_loaded = 0;		//not loaded yet

try_again:
	;

	if (level_num<0)		//secret level
		filename = Current_mission->secret_level_names[-level_num - 1];
	else					//normal level
		filename = Current_mission->level_names[level_num - 1];

#if defined(DXX_BUILD_DESCENT_I)
	if (!convert_ext(filename,"end"))
		return;
#elif defined(DXX_BUILD_DESCENT_II)
	if (!convert_ext(filename,"END"))
		Error("Error converting filename <%s> for endlevel data\n",static_cast<const char *>(filename));
#endif

	auto ifile = PHYSFSX_openReadBuffered(filename).first;

	if (!ifile) {

		convert_ext(filename,"txb");
		if (!strcmp(filename, Current_mission->briefing_text_filename) || 
			!strcmp(filename, Current_mission->ending_text_filename))
                    return;	// Don't want to interpret the briefing as an end level sequence!

		ifile = PHYSFSX_openReadBuffered(filename).first;

		if (!ifile) {
			if (level_num==1) {
#if defined(DXX_BUILD_DESCENT_II)
				con_printf(CON_DEBUG, "Cannot load file text of binary version of <%s>",static_cast<const char *>(filename));
				endlevel_data_loaded = 0; // won't be able to play endlevel sequence
#endif
				return;
			}
			else {
				level_num = 1;
				goto try_again;
			}
		}

		have_binary = 1;
	}

	//ok...this parser is pretty simple.  It ignores comments, but
	//everything else must be in the right place

	var = 0;

	for (PHYSFSX_gets_line_t<LINE_LEN> line; PHYSFSX_fgets(line, ifile);)
	{
		if (have_binary)
			decode_text_line (line);

		if ((p=strchr(line,';'))!=NULL)
			*p = 0;		//cut off comment

		for (p = line; isspace(static_cast<unsigned>(*p)); ++p)
			;
		if (!*p)		//empty line
			continue;
		auto ns = p;
		for (auto p2 = p; *p2; ++p2)
			if (!isspace(static_cast<unsigned>(*p2)))
				ns = p2;
		*++ns = 0;

		switch (var) {

			case 0: {						//ground terrain
				palette_array_t pal;
				terrain_bm_instance.reset();
				if (const auto iff_error{iff_read_bitmap(p, terrain_bm_instance, &pal)}; iff_error != iff_status_code::no_error)
				{
					con_printf(CON_DEBUG, "Can't load exit terrain from file %s: IFF error: %s",
                                                p, iff_errormsg(iff_error));
					endlevel_data_loaded = 0; // won't be able to play endlevel sequence
					return;
				}
				terrain_bitmap = &terrain_bm_instance;
				gr_remap_bitmap_good(terrain_bm_instance, pal, iff_transparent_color, -1);
				break;
			}

			case 1:							//height map

				load_terrain(p);
				break;


			case 2:

				sscanf(p,"%d,%d",&exit_point_bmx,&exit_point_bmy);
				break;

			case 3:							//exit heading

				exit_angles.h = i2f(atoi(p))/360;
				break;

			case 4: {						//planet bitmap
				palette_array_t pal;
				satellite_bm_instance.reset();
				if (const auto iff_error{iff_read_bitmap(p, satellite_bm_instance, &pal)}; iff_error != iff_status_code::no_error)
				{
					con_printf(CON_DEBUG, "Can't load exit satellite from file %s: IFF error: %s",
                                                p, iff_errormsg(iff_error));
					endlevel_data_loaded = 0; // won't be able to play endlevel sequence
					return;
				}

				satellite_bitmap = &satellite_bm_instance;
				gr_remap_bitmap_good(satellite_bm_instance, pal, iff_transparent_color, -1);

				break;
			}

			case 5:							//earth pos
			case 7: {						//station pos
				vms_angvec ta;
				int pitch,head;

				sscanf(p,"%d,%d",&head,&pitch);

				ta.h = i2f(head)/360;
				ta.p = -i2f(pitch)/360;
				ta.b = 0;

				const auto &&tm = vm_angles_2_matrix(ta);

				if (var==5)
					satellite_pos = tm.fvec;
				else
					station_pos = tm.fvec;

				break;
			}

			case 6:						//planet size
				satellite_size = i2f(atoi(p));
				break;
		}

		var++;

	}

	Assert(var == NUM_VARS);


	// OK, now the data is loaded.  Initialize everything

	//find the exit sequence by searching all segments for a side with
	//children == -2

	const auto &&exit_segside = find_exit_segment_side(vcsegptridx);
	const icsegidx_t &exit_segnum = exit_segside.first;
	const auto &exit_side = exit_segside.second;

	PlayerUniqueEndlevelState.exit_segnum = exit_segnum;
	if (exit_segnum == segment_none)
		return;

	const auto &&exit_seg = vmsegptr(exit_segnum);
	auto &vcvertptr = Vertices.vcptr;
	mine_exit_point = compute_segment_center(vcvertptr, exit_seg);
	extract_orient_from_segment(vcvertptr, mine_exit_orient, exit_seg);
	mine_side_exit_point = compute_center_point_on_side(vcvertptr, exit_seg, exit_side);

	mine_ground_exit_point = vm_vec_scale_add(mine_exit_point, mine_exit_orient.uvec, -i2f(20));

	//compute orientation of surface
	{
		auto &&exit_orient = vm_angles_2_matrix(exit_angles);
		vm_transpose_matrix(exit_orient);
		vm_matrix_x_matrix(surface_orient,mine_exit_orient,exit_orient);

		vms_matrix tm = vm_transposed_matrix(surface_orient);
		const auto tv0 = vm_vec_rotate(station_pos,tm);
		station_pos = vm_vec_scale_add(mine_exit_point, tv0, STATION_DIST);

		const auto tv = vm_vec_rotate(satellite_pos,tm);
		satellite_pos = vm_vec_scale_add(mine_exit_point, tv, SATELLITE_DIST);

		const auto tm2{vm_vector_to_matrix_u(tv, surface_orient.uvec)};
		satellite_upvec = vm_vec_copy_scale(tm2.uvec, SATELLITE_HEIGHT);
	}
	endlevel_data_loaded = 1;
}

}
