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



#include "asteroid/asteroid.h"
#include "debugconsole/console.h"
#include "fireball/fireballs.h"
#include "freespace.h"
#include "gamesnd/gamesnd.h"
#include "globalincs/linklist.h"
#include "globalincs/systemvars.h"
#include "hud/hud.h"
#include "hud/hudescort.h"
#include "hud/hudgauges.h"
#include "hud/hudtarget.h"
#include "iff_defs/iff_defs.h"
#include "io/timer.h"
#include "localization/localize.h"
#include "math/staticrand.h"
#include "math/vecmat.h"
#include "model/model.h"
#include "network/multi.h"
#include "network/multimsgs.h"
#include "network/multiutil.h"
#include "object/objcollide.h"
#include "object/object.h"
#include "parse/parselo.h"
#include "particle/ParticleEffect.h"
#include "scripting/global_hooks.h"
#include "particle/hosts/EffectHostVector.h"
#include "render/3d.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "ship/shiphit.h"
#include "stats/scoring.h"
#include "weapon/weapon.h"

#include <algorithm>
#include <random>

#define			ASTEROID_OBJ_USED	(1<<0)				// flag used in asteroid_obj struct
#define			MAX_ASTEROID_OBJS	MAX_ASTEROIDS		// max number of asteroids tracked in asteroid list
asteroid_obj	Asteroid_objs[MAX_ASTEROID_OBJS];	// array used to store asteroid object indexes
asteroid_obj	Asteroid_obj_list;						// head of linked list of asteroid_obj structs

// used for randomly generating debris type when there are multiple sizes.
const float SMALL_DEBRIS_WEIGHT = 8.0f;
const float MEDIUM_DEBRIS_WEIGHT = 4.0f;
const float LARGE_DEBRIS_WEIGHT = 1.0f;

int	Asteroids_enabled = 1;
int	Num_asteroids = 0;

// Contains information for asteroid and debris objects for use in asteroid fields
SCP_vector< asteroid_info > Asteroid_info;
asteroid			Asteroids[MAX_ASTEROIDS];
asteroid_field	Asteroid_field;


static particle::ParticleEffectHandle 		Asteroid_impact_explosion_ani;
char	Asteroid_icon_closeup_model[NAME_LENGTH];
vec3d	Asteroid_icon_closeup_position;
float	Asteroid_icon_closeup_zoom;	

#define	ASTEROID_CHECK_WRAP_TIMESTAMP			2000	// how often an asteroid gets checked for wrapping
#define	ASTEROID_UPDATE_COLLIDE_TIMESTAMP	2000	// how often asteroid is checked for impending collisions with escort ships
#define	ASTEROID_MIN_COLLIDE_TIME				24		// time in seconds to check for asteroid colliding

typedef struct asteroid_target {
	int objnum;
	int signature;
	TIMESTAMP throw_stamp;
	int incoming_asteroids;
} asteroid_target;

// if default throwing behavior is enabled, then this should always have at most 1 target or possibly none, determined the retail way
// if not, then this is whatever number of mission-specified ships (after they arrive, list is sanitized when they exit)
SCP_vector<asteroid_target> Asteroid_targets;


/**
 * Return number of asteroids expected to collide with a ship.
 */
static int count_incident_asteroids(int target_objnum)
{
	object* asteroid_objp;
	int		count = 0;

	Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "count_incident_asteroids() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");

	for (auto aop: list_range(&Asteroid_obj_list)) {
		asteroid_objp = &Objects[aop->objnum];
		if (asteroid_objp->flags[Object::Object_Flags::Should_be_dead])
			continue;

		if (asteroid_objp->type == OBJ_ASTEROID) {
			asteroid* asp = &Asteroids[asteroid_objp->instance];

			if (asp->target_objnum == target_objnum) {
				count++;
			}
		}
	}

	return count;
}

// add a ship as a new asteroid target
void asteroid_add_target(object* objp) {
	asteroid_target new_target;
	new_target.objnum = OBJ_INDEX(objp);
	new_target.signature = objp->signature;
	new_target.throw_stamp = _timestamp(Random::next(500, 2000));
	new_target.incoming_asteroids = count_incident_asteroids(OBJ_INDEX(objp)); // this *should* normally be 0

	Asteroid_targets.push_back(new_target);
}

/**
 * Force updating of pair stuff for asteroid *objp.
 */
static void asteroid_update_collide(object *objp)
{
	// Asteroid has wrapped, update collide objnum and flags
	Asteroids[objp->instance].collide_objnum = -1;
	Asteroids[objp->instance].collide_objsig = -1;
	OBJ_RECALC_PAIRS(objp);	
}

/**
 * Clear out the ::Asteroid_obj_list
 */
static void asteroid_obj_list_init()
{
	int i;

	list_init(&Asteroid_obj_list);
	for ( i = 0; i < MAX_ASTEROID_OBJS; i++ ) {
		Asteroid_objs[i].flags = 0;
	}
}

/**
 * Add a node from the Asteroid_obj_list.  
 * Only called from ::weapon_create()
 */
static int asteroid_obj_list_add(int objnum)
{
	int index;

	Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "asteroid_obj_list_add() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");

	asteroid *cur_asteroid = &Asteroids[Objects[objnum].instance];
	index = (int)(cur_asteroid - Asteroids);

	Assert(index >= 0 && index < MAX_ASTEROID_OBJS);
	Assert(!(Asteroid_objs[index].flags & ASTEROID_OBJ_USED));

	Asteroid_objs[index].flags = 0;
	Asteroid_objs[index].objnum = objnum;
	list_append(&Asteroid_obj_list, &Asteroid_objs[index]);
	Asteroid_objs[index].flags |= ASTEROID_OBJ_USED;

	return index;
}

/**
 * Remove a node from the Asteroid_obj_list.  
 * Only called from ::weapon_delete()
 */
static void asteroid_obj_list_remove(object * obj)
{
	int index = obj->instance;

	Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "asteroid_obj_list_remove() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");

	Assert(index >= 0 && index < MAX_ASTEROID_OBJS);
	Assert(Asteroid_objs[index].flags & ASTEROID_OBJ_USED);

	list_remove(&Asteroid_obj_list, &Asteroid_objs[index]);	
	Asteroid_objs[index].flags = 0;
}


/**
 * Prevent speed from getting too huge so it's hard to catch up to an asteroid.
 */
static float asteroid_cap_speed(int asteroid_info_index, float speed)
{
	float max, double_max;

	Assert( asteroid_info_index < (int)Asteroid_info.size() );

	max = Asteroid_info[asteroid_info_index].max_speed;
	double_max = max * 2;

	while (speed > double_max){
		speed *= 0.5f;
	}
	
	if (speed > max){
		speed *= 0.75f;
	}

	return speed;
}

/**
 * Check if asteroid is within inner bound
 * @return 0 if not inside or no inner bound, 1 if inside inner bound
 */
static bool asteroid_in_inner_bound(asteroid_field *asfieldp, vec3d *pos, float delta) {

	if (!asfieldp->has_inner_bound) {
		return false;
	}

	if (Fix_asteroid_bounding_box_check) {
		return (pos->xyz.x > asfieldp->inner_min_bound.xyz.x + delta) && (pos->xyz.x < asfieldp->inner_max_bound.xyz.x - delta) &&
			(pos->xyz.y > asfieldp->inner_min_bound.xyz.y + delta) && (pos->xyz.y < asfieldp->inner_max_bound.xyz.y - delta) &&
			(pos->xyz.z > asfieldp->inner_min_bound.xyz.z + delta) && (pos->xyz.z < asfieldp->inner_max_bound.xyz.z - delta);
	} else {
		return (pos->xyz.x > asfieldp->inner_min_bound.xyz.x - delta) && (pos->xyz.x < asfieldp->inner_max_bound.xyz.x + delta) &&
			(pos->xyz.y > asfieldp->inner_min_bound.xyz.y - delta) && (pos->xyz.y < asfieldp->inner_max_bound.xyz.y + delta) &&
			(pos->xyz.z > asfieldp->inner_min_bound.xyz.z - delta) && (pos->xyz.z < asfieldp->inner_max_bound.xyz.z + delta);
	}

}

static bool asteroid_is_ship_inside_field(asteroid_field* asfieldp, vec3d* pos, float radius) {

	radius *= 2.0f;

	if (Fix_asteroid_bounding_box_check) {
		return pos->xyz.x - radius > Asteroid_field.min_bound.xyz.x && pos->xyz.x + radius < Asteroid_field.max_bound.xyz.x &&
			   pos->xyz.y - radius > Asteroid_field.min_bound.xyz.y && pos->xyz.y + radius < Asteroid_field.max_bound.xyz.y &&
			   pos->xyz.z - radius > Asteroid_field.min_bound.xyz.z && pos->xyz.z + radius < Asteroid_field.max_bound.xyz.z &&
			   !asteroid_in_inner_bound(asfieldp, pos, radius);
	} else {
		return pos->xyz.x + radius > Asteroid_field.min_bound.xyz.x && pos->xyz.x - radius < Asteroid_field.max_bound.xyz.x &&
			   pos->xyz.y + radius > Asteroid_field.min_bound.xyz.y && pos->xyz.y - radius < Asteroid_field.max_bound.xyz.y &&
			   pos->xyz.z + radius > Asteroid_field.min_bound.xyz.z && pos->xyz.z - radius < Asteroid_field.max_bound.xyz.z &&
			   !asteroid_in_inner_bound(asfieldp, pos, radius);
	}

}

/**
 * Repositions asteroid outside the inner box on all 3 axes
 *
 * Moves to the other side of the inner box a distance delta from edge of box
 */
static void inner_bound_pos_fixup(asteroid_field *asfieldp, vec3d *pos)
{
	if (!asteroid_in_inner_bound(asfieldp, pos, 0)) {
		return;
	}

	float dist1, dist2;
	int axis;

	for (axis=0; axis<3; axis++) {
		dist1 = pos->a1d[axis] - asfieldp->inner_min_bound.a1d[axis];
		dist2 = asfieldp->inner_max_bound.a1d[axis] - pos->a1d[axis];
		Assert(dist1 >= 0 && dist2 >= 0);

		if (dist1 < dist2) {
			pos->a1d[axis] = asfieldp->inner_max_bound.a1d[axis] + dist1;
		} else {
			pos->a1d[axis] = asfieldp->inner_min_bound.a1d[axis] - dist2;
		}
	}
}

/**
* Randomly select an asteroid subtype
*/
const SCP_string& pick_random_asteroid_type()
{
	return Asteroid_field.field_asteroid_type[Random::next(static_cast<int>(Asteroid_field.field_asteroid_type.size()))];
}

int get_asteroid_subtype_index_by_name(const SCP_string &name, int asteroid_idx) {
	asteroid_info* this_asteroid = &Asteroid_info[asteroid_idx];
	for (int i = 0; i < static_cast<int>(this_asteroid->subtypes.size()); i++) {
		if (this_asteroid->subtypes[i].type_name == name) {
			return i;
		}
	}

	return -1;
}

/**
 * Create a single asteroid 
 */
object *asteroid_create(asteroid_field *asfieldp, int asteroid_type, int asteroid_subtype, bool check_visibility)
{
	int				n, objnum;
	matrix			orient;
	object			*objp;
	asteroid			*asp;
	asteroid_info	*asip;
	vec3d			pos, delta_bound;
	angles			angs;
	float				radius;
	ushort			signature;
	int				rand_base;

	// bogus
	if(asfieldp == nullptr) {
		return nullptr;
	}

	for (n=0; n<MAX_ASTEROIDS; n++) {
		if (!(Asteroids[n].flags & AF_USED)) {
			break;
		}
	}

	if (n >= MAX_ASTEROIDS) {
		nprintf(("Warning","Could not create asteroid, no more slots left\n"));
		return nullptr;
	}

	if (!SCP_vector_inbounds(Asteroid_info, asteroid_type)) {
		return nullptr;
	}

	// HACK: multiplayer asteroid subtype always 0 to keep subtype in sync
	if ( Game_mode & GM_MULTIPLAYER) {
		asteroid_subtype = 0;
	}	

	asip = &Asteroid_info[asteroid_type];

	if (!SCP_vector_inbounds(asip->subtypes, asteroid_subtype)) {
		return nullptr;
	}

	// if the model is not loaded then abort
	if(asip->subtypes[asteroid_subtype].model_number == -1) {
		return nullptr;
	}	

	asp = &Asteroids[n];
	asp->asteroid_type = asteroid_type;
	asp->asteroid_subtype = asteroid_subtype;
	asp->flags = 0;
	asp->flags |= AF_USED;
	asp->check_for_wrap = _timestamp_rand(0, ASTEROID_CHECK_WRAP_TIMESTAMP);
	asp->check_for_collide = _timestamp_rand(0, ASTEROID_UPDATE_COLLIDE_TIMESTAMP);
	asp->final_death_time = TIMESTAMP::invalid();
	asp->collide_objnum = -1;
	asp->collide_objsig = -1;
	asp->target_objnum = -1;

	radius = model_get_radius(asip->subtypes[asteroid_subtype].model_number);

	vm_vec_sub(&delta_bound, &asfieldp->max_bound, &asfieldp->min_bound);

	// for multiplayer, we want to do a static_rand so that everything behaves the same on all machines
	signature = 0;
	rand_base = 0;
	if ( Game_mode & GM_NORMAL ) {
		pos.xyz.x = asfieldp->min_bound.xyz.x + delta_bound.xyz.x * frand();
		pos.xyz.y = asfieldp->min_bound.xyz.y + delta_bound.xyz.y * frand();
		pos.xyz.z = asfieldp->min_bound.xyz.z + delta_bound.xyz.z * frand();

		inner_bound_pos_fixup(asfieldp, &pos);
		angs.p = frand() * PI2;
		angs.b = frand() * PI2;
		angs.h = frand() * PI2;
	} else {
		signature = multi_assign_network_signature( MULTI_SIG_ASTEROID );
		rand_base = signature;

		pos.xyz.x = asfieldp->min_bound.xyz.x + delta_bound.xyz.x * static_randf( rand_base++ );
		pos.xyz.y = asfieldp->min_bound.xyz.y + delta_bound.xyz.y * static_randf( rand_base++ );
		pos.xyz.z = asfieldp->min_bound.xyz.z + delta_bound.xyz.z * static_randf( rand_base++ );

		inner_bound_pos_fixup(asfieldp, &pos);
		angs.p = static_randf( rand_base++ ) * PI2;
		angs.b = static_randf( rand_base++ ) * PI2;
		angs.h = static_randf( rand_base++ ) * PI2;
	}

	// If the generated asteroid position is within the player view then abort
	// I'm unsure how Eyepoint is handled for multiplayer, so this may need to move up into the SP
	// only section - Mjn
	if (check_visibility && asteroid_is_within_view(&pos, -1.0f, true)) {
		return nullptr;
	}

	vm_angles_2_matrix(&orient, &angs);
    flagset<Object::Object_Flags> asteroid_default_flagset;
    asteroid_default_flagset += Object::Object_Flags::Renders;
    asteroid_default_flagset += Object::Object_Flags::Physics;
    asteroid_default_flagset += Object::Object_Flags::Collides;
    
    objnum = obj_create(OBJ_ASTEROID, -1, n, &orient, &pos, radius, asteroid_default_flagset, false);
	
	if ( (objnum == -1) || (objnum >= MAX_OBJECTS) ) {
		mprintf(("Couldn't create asteroid -- out of object slots\n"));
		return NULL;
	}

	asp->objnum = objnum;
	asp->model_instance_num = -1;

	if (model_get(asip->subtypes[asteroid_subtype].model_number)->flags & PM_FLAG_HAS_INTRINSIC_MOTION) {
		asp->model_instance_num = model_create_instance(objnum, asip->subtypes[asteroid_subtype].model_number);
	}

	// Add to Asteroid_used_list
	asteroid_obj_list_add(objnum);

	objp = &Objects[objnum];

	if ( Game_mode & GM_MULTIPLAYER ){
		objp->net_signature = signature;
	}

	Num_asteroids++;

	if (radius < 1.0) {
		radius = 1.0f;
	}

	vec3d rotvel;
	if ( Game_mode & GM_NORMAL ) {
		vm_vec_rand_vec_quick(&rotvel);
		vm_vec_scale(&rotvel, asip->rotational_vel_multiplier * (frand()/4.0f + 0.1f));
		objp->phys_info.rotvel = rotvel;
		vm_vec_rand_vec_quick(&objp->phys_info.vel);
	} else {
		static_randvec( rand_base++, &rotvel );
		vm_vec_scale(&rotvel, asip->rotational_vel_multiplier * (static_randf(rand_base++)/4.0f + 0.1f));
		objp->phys_info.rotvel = rotvel;
		static_randvec( rand_base++, &objp->phys_info.vel );
	}


	float speed;

	if ( Game_mode & GM_NORMAL ) {
		speed = asteroid_cap_speed(asteroid_type, asfieldp->speed*frand_range(0.5f + (float) Game_skill_level/NUM_SKILL_LEVELS, 2.0f + (float) (2*Game_skill_level)/NUM_SKILL_LEVELS));
	} else {
		speed = asteroid_cap_speed(asteroid_type, asfieldp->speed*static_randf_range(rand_base++, 0.5f + (float) Game_skill_level/NUM_SKILL_LEVELS, 2.0f + (float) (2*Game_skill_level)/NUM_SKILL_LEVELS));
	}
	
	vm_vec_scale(&objp->phys_info.vel, speed);
	objp->phys_info.desired_vel = objp->phys_info.vel;

	// blow out his reverse thrusters. Or drag, same thing.
	objp->phys_info.rotdamp = 10000.0f;
	objp->phys_info.flags |= (PF_DEAD_DAMP | PF_BALLISTIC);
	objp->phys_info.gravity_const = asip->gravity_const;

	// Fill in the max_vel field, so the collision pair stuff knows
	// how fast this can move maximum in order to throw out collisions.
	// This is in local coordinates, so Z is forward velocity.
	objp->phys_info.max_vel.xyz.x = 0.0f;
	objp->phys_info.max_vel.xyz.y = 0.0f;
	objp->phys_info.max_vel.xyz.z = vm_vec_mag(&objp->phys_info.desired_vel);
	
	objp->phys_info.mass = model_get(asip->subtypes[asteroid_subtype].model_number)->rad * 700.0f;
	objp->phys_info.I_body_inv.vec.rvec.xyz.x = 1.0f / (objp->phys_info.mass*model_get(asip->subtypes[asteroid_subtype].model_number)->rad);
	objp->phys_info.I_body_inv.vec.uvec.xyz.y = objp->phys_info.I_body_inv.vec.rvec.xyz.x;
	objp->phys_info.I_body_inv.vec.fvec.xyz.z = objp->phys_info.I_body_inv.vec.rvec.xyz.x;
	objp->hull_strength = asip->initial_asteroid_strength * (0.8f + (float)Game_skill_level/NUM_SKILL_LEVELS)/2.0f;

	// ensure vel is valid
	Assert( !vm_is_vec_nan(&objp->phys_info.vel) );	

	if (scripting::hooks::OnAsteroidCreated->isActive()) {
		scripting::hooks::OnAsteroidCreated->run(
			scripting::hook_param_list(
				scripting::hook_param("Asteroid", 'o', objp)));
	}

	return objp;
}

/**
 * Create asteroids when parent_objp blows up.
 */
void asteroid_sub_create(object *parent_objp, int asteroid_type, vec3d *relvec)
{
	object	*new_objp;
	float speed;

	Assert(parent_objp->type == OBJ_ASTEROID);
	int subtype = Asteroids[parent_objp->instance].asteroid_subtype;
	new_objp = asteroid_create(&Asteroid_field, asteroid_type, subtype);

	// New asteroid size doesn't have this subtype, so abort
	if (new_objp == nullptr)
		return;

	if ( MULTIPLAYER_MASTER ){
		send_asteroid_create( new_objp, parent_objp, asteroid_type, relvec );
	}

	//	Now, bash some values.
	vm_vec_scale_add(&new_objp->pos, &parent_objp->pos, relvec, 0.5f * parent_objp->radius);
	float parent_speed = vm_vec_mag_quick(&parent_objp->phys_info.vel);

	if ( parent_speed < 0.1f ) {
		parent_speed = vm_vec_mag_quick(&Asteroid_field.vel);
	}

	new_objp->phys_info.vel = parent_objp->phys_info.vel;
	if ( Game_mode & GM_NORMAL )
		speed = asteroid_cap_speed(asteroid_type, (frand() + 2.0f) * parent_speed);
	else
		speed = asteroid_cap_speed(asteroid_type, (static_randf(new_objp->net_signature)+2.0f) * parent_speed);

	vm_vec_scale_add2(&new_objp->phys_info.vel, relvec, speed);
	if (vm_vec_mag_quick(&new_objp->phys_info.vel) > 80.0f)
		vm_vec_scale(&new_objp->phys_info.vel, 0.5f);

	new_objp->phys_info.desired_vel = new_objp->phys_info.vel;
	vm_vec_scale_add(&new_objp->last_pos, &new_objp->pos, &new_objp->phys_info.vel, -flFrametime);
}

/**
 * Load in an asteroid model
 */
void asteroid_load(int asteroid_info_index, int asteroid_subtype)
{
	int i;
	asteroid_info	*asip;

	Assertion(asteroid_info_index < static_cast<int>(Asteroid_info.size()),
		"Command to load asteroid at index %i, but only %i asteroids exist",
		asteroid_info_index,
		static_cast<int>(Asteroid_info.size()));

	// invalid asteroid index
	if (!SCP_vector_inbounds(Asteroid_info, asteroid_info_index)) {
		return;
	}

	asip = &Asteroid_info[asteroid_info_index];

	// This asteroid does not have this subtype so just return
	if (!SCP_vector_inbounds(asip->subtypes, asteroid_subtype)) {
		return;
	}

	if ( !VALID_FNAME(asip->subtypes[asteroid_subtype].pof_filename) )
		return;

	//Check if the model is already loaded
	if (asip->subtypes[asteroid_subtype].model_number >= 0)
		return;

	asip->subtypes[asteroid_subtype].model_number = model_load( asip->subtypes[asteroid_subtype].pof_filename );

	if (asip->subtypes[asteroid_subtype].model_number >= 0)
	{
		polymodel *pm = model_get(asip->subtypes[asteroid_subtype].model_number);
		
		if ( asip->num_detail_levels != pm->n_detail_levels )
		{
			if ( !Is_standalone )
			{
				// just log to file for standalone servers
				Warning(LOCATION, "For asteroid '%s', detail level mismatch (POF has %d, table has %d)", asip->name, pm->n_detail_levels, asip->num_detail_levels );
			}
			else
			{
				nprintf(("Warning",  "For asteroid '%s', detail level mismatch (POF has %d, table has %d)\n", asip->name, pm->n_detail_levels, asip->num_detail_levels ));
			}
		}	
		// Stuff detail level distances.
		for ( i=0; i<pm->n_detail_levels; i++ )
			pm->detail_depth[i] = (i < asip->num_detail_levels) ? i2fl(asip->detail_distance[i]) : 0.0f;
	}
}

/**
 * Create all the asteroids for the mission
 */
void asteroid_create_all()
{
	int i, idx;

	// ship_debris_odds_table keeps track of debris type of the next debris piece
	// each different type (size) of debris piece has a diffenent weight, smaller weighted more heavily than larger.
	// choose next type from table ship_debris_odds_table by Random::next()%max_weighted_range,
	// the threshold *below* which the debris type is selected.
	struct ShipDebrisOdds {
		float random_threshold;
		int debris_type;
	};
	std::vector<ShipDebrisOdds> shipDebrisOddsTable(Asteroid_field.field_debris_type.size());

	float max_weighted_range = 0.0f;

	if (!Asteroids_enabled)
		return;

	if (Asteroid_field.num_initial_asteroids <= 0 ) {
		return;
	}

	if ((Asteroid_field.debris_genre == DG_DEBRIS && Asteroid_field.field_debris_type.empty()) ||
		(Asteroid_field.debris_genre == DG_ASTEROID && Asteroid_field.field_asteroid_type.empty())) {
		Warning(LOCATION, "An asteroid field is enabled, but no asteroid types were enabled.");
		return;
	}

	//Check that the asteroid field bounds are valid - Mjn
	if (Asteroid_field.min_bound.xyz.x >= Asteroid_field.max_bound.xyz.x) {
		Warning(LOCATION, "Asteroid field Outer Bound Min X is greater than or equal to Max X. Aborting asteroid creation!");
		return;
	}
	if (Asteroid_field.min_bound.xyz.y >= Asteroid_field.max_bound.xyz.y) {
		Warning(LOCATION, "Asteroid field Outer Bound Min Y is greater than or equal to Max Y. Aborting asteroid creation!");
		return;
	}
	if (Asteroid_field.min_bound.xyz.z >= Asteroid_field.max_bound.xyz.z) {
		Warning(LOCATION, "Asteroid field Outer Bound Min Z is greater than or equal to Max Z. Aborting asteroid creation!");
		return;
	}
	if (Asteroid_field.inner_min_bound.xyz.x >= Asteroid_field.inner_max_bound.xyz.x) {
		Warning(LOCATION, "Asteroid field Inner Bound Min X is greater than or equal to Max X. Aborting asteroid creation!");
		return;
	}
	if (Asteroid_field.inner_min_bound.xyz.y >= Asteroid_field.inner_max_bound.xyz.y) {
		Warning(LOCATION, "Asteroid field Inner Bound Min Y is greater than or equal to Max Y. Aborting asteroid creation!");
		return;
	}
	if (Asteroid_field.inner_min_bound.xyz.z >= Asteroid_field.inner_max_bound.xyz.z) {
		Warning(LOCATION, "Asteroid field Inner Bound Min Z is greater than or equal to Max Z. Aborting asteroid creation!");
		return;
	}

	int max_asteroids = Asteroid_field.num_initial_asteroids; // * (1.0f - 0.1f*(MAX_DETAIL_VALUE-Detail.asteroid_density)));

	int num_debris_types = 0;
	int num_valid = 0;	// count of valid (non-(-1)) entries written into shipDebrisOddsTable

	// get number of debris types
	if (Asteroid_field.debris_genre == DG_DEBRIS) {
		num_debris_types = static_cast<int>(Asteroid_field.field_debris_type.size());

		// Calculate the odds table; skip invalid entries (field_debris_type[idx] == -1 means
		// missionparse invalidated the entry because the type no longer exists in Asteroid_info)
		for (idx=0; idx<num_debris_types; idx++) {
			if (Asteroid_field.field_debris_type[idx] == -1)
				continue;
			float debris_weight = Asteroid_info[Asteroid_field.field_debris_type[idx]].spawn_weight;
			shipDebrisOddsTable[num_valid].random_threshold = max_weighted_range + debris_weight;
			shipDebrisOddsTable[num_valid].debris_type = Asteroid_field.field_debris_type[idx];
			max_weighted_range += debris_weight;
			num_valid++;
		}
	}

	// Load Asteroid/debris models
	if (Asteroid_field.debris_genre == DG_DEBRIS) {
		for (idx=0; idx<num_debris_types; idx++) {
			if (Asteroid_field.field_debris_type[idx] != -1)
				asteroid_load(Asteroid_field.field_debris_type[idx], 0);
		}
	} else {
		for (const auto& subtype : Asteroid_field.field_asteroid_type) {
			asteroid_load(ASTEROID_TYPE_SMALL, get_asteroid_subtype_index_by_name(subtype, ASTEROID_TYPE_SMALL));
			asteroid_load(ASTEROID_TYPE_MEDIUM, get_asteroid_subtype_index_by_name(subtype, ASTEROID_TYPE_MEDIUM));
			asteroid_load(ASTEROID_TYPE_LARGE, get_asteroid_subtype_index_by_name(subtype, ASTEROID_TYPE_LARGE));
		}
	}

	// load all the asteroid/debris pieces
	for (i=0; i<max_asteroids; i++) {
		if (Asteroid_field.debris_genre == DG_ASTEROID) {
			// For asteroid, load only large asteroids

			// get a valid subtype
			int subtype = get_asteroid_subtype_index_by_name(pick_random_asteroid_type(), ASTEROID_TYPE_LARGE);

			if (subtype >= 0)
				asteroid_create(&Asteroid_field, ASTEROID_TYPE_LARGE, subtype);
		} else {
			Assert(num_debris_types > 0);

			float rand_choice = frand() * max_weighted_range;

			for (idx = 0; idx < num_valid; idx++) {
				// for ship debris, choose type according to odds table
				if (rand_choice < shipDebrisOddsTable[idx].random_threshold) {
					asteroid_create(&Asteroid_field, shipDebrisOddsTable[idx].debris_type, 0);
					break;
				}
			}
		}
	}
}

// instantly deletes all asteroids in a mission before moving on.
// Uses obj_delete_all_that_should_be_dead() so this method has a side effect
// of also deleting non-asteroid objects marked as dead in the same frame. Usually this is
// desired but it's worth knowing ahead of time. -Mjn
void remove_all_asteroids()
{
	for (int i = 0; i < MAX_ASTEROIDS; i++) {
		if (Asteroids[i].flags & AF_USED) {
			Asteroids[i].flags &= ~AF_USED;
			Assert(Asteroids[i].objnum >= 0 && Asteroids[i].objnum < MAX_OBJECTS);
			Objects[Asteroids[i].objnum].flags.set(Object::Object_Flags::Should_be_dead);
		}
	}
	// This feels hackish, but we need to make sure all the asteroids are actually gone before we continue-Mjn
	obj_delete_all_that_should_be_dead();
}

// will replace any existing asteroid or debris field with an asteroid field
void asteroid_create_asteroid_field(int num_asteroids, int field_type, int asteroid_speed, vec3d o_min, vec3d o_max, bool inner_box, vec3d i_min, vec3d i_max, SCP_vector<SCP_string> asteroid_types)
{
	remove_all_asteroids();

	Asteroid_field.num_initial_asteroids = num_asteroids;

	switch (field_type) {
		case 0:
			Asteroid_field.field_type = FT_PASSIVE;
			Asteroid_field.enhanced_visibility_checks = false;
			break;
		case 1:
			Asteroid_field.field_type = FT_ACTIVE;
			Asteroid_field.enhanced_visibility_checks = false;
			break;
		case 2:
			Asteroid_field.field_type = FT_PASSIVE;
			Asteroid_field.enhanced_visibility_checks = true;
			break;
		case 3:
			Asteroid_field.field_type = FT_ACTIVE;
			Asteroid_field.enhanced_visibility_checks = true;
			break;
		default: //Any other number will just give default behavior
			Asteroid_field.field_type = FT_PASSIVE;
			Asteroid_field.enhanced_visibility_checks = false;
			break;
	}

	vm_vec_rand_vec_quick(&Asteroid_field.vel);
	vm_vec_scale(&Asteroid_field.vel, (float)asteroid_speed);
	Asteroid_field.speed = (float)asteroid_speed;
	Asteroid_field.debris_genre = DG_ASTEROID;

	Asteroid_field.field_debris_type.clear();

	Asteroid_field.field_asteroid_type.clear();

	Asteroid_field.field_asteroid_type = std::move(asteroid_types);

	Asteroid_field.min_bound = o_min;
	Asteroid_field.max_bound = o_max;

	vec3d a_rad;
	vm_vec_sub(&a_rad, &Asteroid_field.max_bound, &Asteroid_field.min_bound);
	vm_vec_scale(&a_rad, 0.5f);
	float b_rad = vm_vec_mag(&a_rad);

	Asteroid_field.bound_rad = MAX(3000.0f, b_rad);

	if (inner_box) {
		Asteroid_field.has_inner_bound = true;
		Asteroid_field.inner_min_bound = i_min;
		Asteroid_field.inner_max_bound = i_max;
	}

	// Only create asteroids if we have some to create
	if ((!Asteroid_field.field_asteroid_type.empty()) && (num_asteroids > 0)) {
		asteroid_create_all();
	}
}

// will replace any existing asteroid or debris field with a debris field
void asteroid_create_debris_field(int num_asteroids, int asteroid_speed, SCP_vector<int> debris_types, vec3d o_min, vec3d o_max, bool enhanced)
{
	remove_all_asteroids();

	Asteroid_field.num_initial_asteroids = num_asteroids;

	Asteroid_field.field_type = FT_PASSIVE;

	Asteroid_field.enhanced_visibility_checks = enhanced;

	vm_vec_rand_vec_quick(&Asteroid_field.vel);
	vm_vec_scale(&Asteroid_field.vel, (float)asteroid_speed);
	Asteroid_field.speed = (float)asteroid_speed;
	Asteroid_field.debris_genre = DG_DEBRIS;

	Asteroid_field.field_debris_type.clear();

	Asteroid_field.field_asteroid_type.clear();

	Asteroid_field.field_debris_type = std::move(debris_types);

	Asteroid_field.min_bound = o_min;
	Asteroid_field.max_bound = o_max;

	vec3d a_rad;
	vm_vec_sub(&a_rad, &Asteroid_field.max_bound, &Asteroid_field.min_bound);
	vm_vec_scale(&a_rad, 0.5f);
	float b_rad = vm_vec_mag(&a_rad);

	Asteroid_field.bound_rad = MAX(3000.0f, b_rad);

	// Only create debris if we have some to create
	if ((!Asteroid_field.field_debris_type.empty()) && (num_asteroids > 0)) {
		asteroid_create_all();
	}
}

/**
 * Init asteroid system for the level, called from ::game_level_init()
 */
void asteroid_level_init()
{
	Asteroid_field = {};

	if (!Fred_running)
	{
		for (auto& ast : Asteroid_info)
			ast.damage_type_idx = ast.damage_type_idx_sav;

		Num_asteroids = 0;
		asteroid_obj_list_init();
		Asteroid_targets.clear();
	}
}

/**
 * Should asteroid wrap from one end of the asteroid field to the other.
 * Multiplayer clients will always return 0 from this function.  We will force a wrap on the clients when server tells us
 *
 * @return !0 if asteroid should be wrapped, 0 otherwise.  
 */
static int asteroid_should_wrap(object *objp, asteroid_field *asfieldp)
{
	if ( MULTIPLAYER_CLIENT )
		return 0;

	if (objp->pos.xyz.x < asfieldp->min_bound.xyz.x) {
		return 1;
	}

	if (objp->pos.xyz.y < asfieldp->min_bound.xyz.y) {
		return 1;
	}

	if (objp->pos.xyz.z < asfieldp->min_bound.xyz.z) {
		return 1;
	}

	if (objp->pos.xyz.x > asfieldp->max_bound.xyz.x) {
		return 1;
	}

	if (objp->pos.xyz.y > asfieldp->max_bound.xyz.y) {
		return 1;
	}

	if (objp->pos.xyz.z > asfieldp->max_bound.xyz.z) {
		return 1;
	}

	// check against inner bound
	if (asfieldp->has_inner_bound) {
		if ( (objp->pos.xyz.x > asfieldp->inner_min_bound.xyz.x) && (objp->pos.xyz.x < asfieldp->inner_max_bound.xyz.x)
		  && (objp->pos.xyz.y > asfieldp->inner_min_bound.xyz.y) && (objp->pos.xyz.y < asfieldp->inner_max_bound.xyz.y)
		  && (objp->pos.xyz.z > asfieldp->inner_min_bound.xyz.z) && (objp->pos.xyz.z < asfieldp->inner_max_bound.xyz.z) ) {

			return 1;
		}
	}

	return 0;
}

/**
 * Generates a potential new position for an asteroid that might wrap
 * from one end of the asteroid field to the other
 */
static void asteroid_wrap_pos(vec3d *pos, asteroid_field *asfieldp)
{
	if (pos->xyz.x < asfieldp->min_bound.xyz.x) {
		pos->xyz.x = asfieldp->max_bound.xyz.x + (pos->xyz.x - asfieldp->min_bound.xyz.x);
	}

	if (pos->xyz.y < asfieldp->min_bound.xyz.y) {
		pos->xyz.y = asfieldp->max_bound.xyz.y + (pos->xyz.y - asfieldp->min_bound.xyz.y);
	}

	if (pos->xyz.z < asfieldp->min_bound.xyz.z) {
		pos->xyz.z = asfieldp->max_bound.xyz.z + (pos->xyz.z - asfieldp->min_bound.xyz.z);
	}

	if (pos->xyz.x > asfieldp->max_bound.xyz.x) {
		pos->xyz.x = asfieldp->min_bound.xyz.x + (pos->xyz.x - asfieldp->max_bound.xyz.x);
	}

	if (pos->xyz.y > asfieldp->max_bound.xyz.y) {
		pos->xyz.y = asfieldp->min_bound.xyz.y + (pos->xyz.y - asfieldp->max_bound.xyz.y);
	}

	if (pos->xyz.z > asfieldp->max_bound.xyz.z) {
		pos->xyz.z = asfieldp->min_bound.xyz.z + (pos->xyz.z - asfieldp->max_bound.xyz.z);
	}

	// wrap on inner bound, check all 3 axes as needed, use of rand ok for multiplayer with send_asteroid_throw()
	inner_bound_pos_fixup(asfieldp, pos);
}


/**
 * Is asteroid targeted? 
 *
 * @return !0 if this asteroid is a target for any ship, otherwise return 0
 */
static int asteroid_is_targeted(object *objp)
{
	ship_obj	*so;
	object	*ship_objp;
	int		asteroid_obj_index;

	asteroid_obj_index=OBJ_INDEX(objp);

	for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
		ship_objp = &Objects[so->objnum];
		if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
			continue;
		if ( Ai_info[Ships[ship_objp->instance].ai_index].target_objnum == asteroid_obj_index ) {
			return 1;
		}
	}
	
	return 0;
}

/**
 * Create an asteroid that will hit object *objp in delta_time seconds
 */
static void asteroid_aim_at_target(object *objp, object *asteroid_objp, float delta_time)
{
	vec3d	predicted_center_pos;
	vec3d	rand_vec;
	float		speed;

	vm_vec_scale_add(&predicted_center_pos, &objp->pos, &objp->phys_info.vel, delta_time);
	vm_vec_rand_vec_quick(&rand_vec);
	vm_vec_scale_add2(&predicted_center_pos, &rand_vec, objp->radius/2.0f);

	vm_vec_add2(&rand_vec, &objp->orient.vec.fvec);
	if (vm_vec_mag_quick(&rand_vec) < 0.1f)
		vm_vec_add2(&rand_vec, &objp->orient.vec.rvec);
	vm_vec_normalize(&rand_vec);

	auto asp = &Asteroids[asteroid_objp->instance];
	speed = Asteroid_info[asp->asteroid_type].max_speed * (frand()/2.0f + 0.5f);
	
	vm_vec_copy_scale(&asteroid_objp->phys_info.vel, &rand_vec, -speed);
	asteroid_objp->phys_info.desired_vel = asteroid_objp->phys_info.vel;
	vm_vec_scale_add(&asteroid_objp->pos, &predicted_center_pos, &asteroid_objp->phys_info.vel, -delta_time);
	vm_vec_scale_add(&asteroid_objp->last_pos, &asteroid_objp->pos, &asteroid_objp->phys_info.vel, -flFrametime);
}

/**
* Sanitizes the Asteroid targets list removing ships that have died/become invalid etc
*/
static void sanitize_asteroid_targets_list() {
	for (size_t i = 0; i < Asteroid_targets.size();) {
		object* target_objp = &Objects[Asteroid_targets[i].objnum];
		if (target_objp->type != OBJ_SHIP || target_objp->signature != Asteroid_targets[i].signature) {
			Asteroid_targets[i] = Asteroid_targets.back();
			Asteroid_targets.pop_back();
		}
		else  // if we needed to cull we should not advance because we just moved a new asteroid target into this spot
			i++;
	}
}

/**
* Checks if an asteroid will be within the player's view
*
**/
bool asteroid_is_within_view(vec3d *pos, float range, bool range_override)
{
	vec3d vec_to_asteroid;

	// Distance and view cone for the position in relation to the player
	float dist = vm_vec_normalized_dir(&vec_to_asteroid, pos, &Eye_position);
	float dot = vm_vec_dot(&Eye_matrix.vec.fvec, &vec_to_asteroid);

	// if asteroid is within view or far enough away then wrap
	if (dot > cos(g3_get_hfov(Proj_fov))) {
		if (range_override) {
			return true;
		} else {
			if (dist < range) {
				return true;
			}
		}
	}

	return false;
}

/**
 * Call once per frame to maybe throw some asteroids at one or more ships.
 *
 */
static void maybe_throw_asteroid()
{
	Assertion(!Asteroid_field.field_asteroid_type.empty(), "maybe_throw_asteroid() called while field_asteroid_type.size was 0; this should never happen, get a coder!");

	for (asteroid_target& target : Asteroid_targets) {
		if (!timestamp_elapsed(target.throw_stamp))
			continue;

		object* target_objp = &Objects[target.objnum];
		if (!asteroid_is_ship_inside_field(&Asteroid_field, &target_objp->pos, target_objp->radius))
			continue;
		
		// This should've been greater equal, but alas...
		if (target.incoming_asteroids > The_mission.ai_profile->max_incoming_asteroids[Game_skill_level])
			continue;

		nprintf(("AI", "Incoming asteroids to %s: %i\n", Ships[target_objp->instance].ship_name, target.incoming_asteroids));

		target.throw_stamp = _timestamp(1000 + 1200 * target.incoming_asteroids / (Game_skill_level+1));

		// get a valid subtype
		int subtype = get_asteroid_subtype_index_by_name(pick_random_asteroid_type(), ASTEROID_TYPE_LARGE);

		// this really shouldn't happen but just in case...
		if (subtype < 0)
			return;

		object *ast_objp = asteroid_create(&Asteroid_field, ASTEROID_TYPE_LARGE, subtype, Asteroid_field.enhanced_visibility_checks);
		if (ast_objp != nullptr) {
			asteroid_aim_at_target(target_objp, ast_objp, ASTEROID_MIN_COLLIDE_TIME + frand() * 20.0f);

			// if asteroid is inside inner bound, kill it
			if (asteroid_in_inner_bound(&Asteroid_field, &ast_objp->pos, 0.0f)) {
				ast_objp->flags.set(Object::Object_Flags::Should_be_dead);
			} else {
				Asteroids[ast_objp->instance].target_objnum = target.objnum;
				target.incoming_asteroids++;

				if ( MULTIPLAYER_MASTER ) {
					send_asteroid_throw( ast_objp );
				}
			}
		}
	}

}

/**
 * Delete asteroid from Asteroid_used_list
 */
void asteroid_delete( object * obj )
{
	int		num;
	asteroid	*asp;

	num = obj->instance;
	Assert( Asteroids[num].objnum == OBJ_INDEX(obj));

	asp = &Asteroids[num];

	if (asp->model_instance_num >= 0) {
		model_delete_instance(asp->model_instance_num);
		asp->model_instance_num = -1;
	}

	if (asp->target_objnum >= 0) {
		for (asteroid_target& target : Asteroid_targets) {
			if (asp->target_objnum == target.objnum)
				target.incoming_asteroids--;
		}
	}

	asp->flags = 0;
	Num_asteroids--;
	Assert(Num_asteroids >= 0);

	asteroid_obj_list_remove( obj );
}

/**
 * See if we should reposition the asteroid.
 * Only reposition if oustide the bounding volume and the player isn't looking towards the asteroid.
 */
static void asteroid_maybe_reposition(object *objp, asteroid_field *asfieldp)
{
	// passive field does not wrap if there is no gravity
	if (IS_VEC_NULL(&The_mission.gravity) && (asfieldp->field_type == FT_PASSIVE)) {
		return;
	}

	if (asteroid_should_wrap(objp, asfieldp)) {

		// Generate a possible new position if we do end up wrapping, but don't move the asteroid yet
		vec3d new_pos = objp->pos;
		asteroid_wrap_pos(&new_pos, asfieldp);

		bool wrap = false;
		bool reverse = false;

		// If asteroid is targeted then we cannot wrap because they player would see it and say "SHENANIGANS!"
		// So we bail
		if (asteroid_is_targeted(objp)) {
			return;
		}

		// if asteroid is not within view...
		if (!asteroid_is_within_view(&objp->pos, asfieldp->bound_rad, asfieldp->enhanced_visibility_checks)) {

			// if asteroid new position is within view then reverse velocity, otherwise wrap
			if (asteroid_is_within_view(&new_pos, (asfieldp->bound_rad * 1.3f), asfieldp->enhanced_visibility_checks)) {

				// if the mission has gravity, then we can't reverse. So make sure gravity is null
				if (IS_VEC_NULL(&The_mission.gravity)) {
					reverse = true;
				}

			} else {
				wrap = true;
			}

		}

		// we can wrap, but we have too many asteroids, so destroy this one instead
		if (wrap && (Num_asteroids > MAX_ASTEROIDS - 10)) {
			objp->flags.set(Object::Object_Flags::Should_be_dead);
			return;
		}

		// Reverse instead of wrap!
		if (reverse) {
			vm_vec_copy_scale(&objp->phys_info.vel, &objp->phys_info.vel, -1.0f);
			objp->phys_info.desired_vel = objp->phys_info.vel;
			Asteroids[objp->instance].target_objnum = -1;
			return;
		}

		// We can wrap, so wrap!
		if (wrap) {
			objp->pos = new_pos;
			asteroid *astp = &Asteroids[objp->instance];

			if (asfieldp->field_type == FT_ACTIVE) {
				// this doesnt count as a thrown asteroid anymore
				for (asteroid_target &target : Asteroid_targets)
					if (target.objnum == astp->target_objnum)
						target.incoming_asteroids--;

				astp->target_objnum = -1;
			}

			// update last pos (after vel is known)
			vm_vec_scale_add(&objp->last_pos, &objp->pos, &objp->phys_info.vel, -flFrametime);

			asteroid_update_collide(objp);

			if (asfieldp->field_type == FT_ACTIVE) {
				if (MULTIPLAYER_MASTER)
					send_asteroid_throw(objp);
			}
		}
	}
}

static void lerp(float *goal, float f1, float f2, float scale)
{
	*goal = (f2 - f1) * scale + f1;
}

void asteroid_process_pre( object *objp )
{
	if (Asteroids_enabled) {
		//	Make vel chase desired_vel
		lerp(&objp->phys_info.vel.xyz.x, objp->phys_info.vel.xyz.x, objp->phys_info.desired_vel.xyz.x, flFrametime);
		lerp(&objp->phys_info.vel.xyz.y, objp->phys_info.vel.xyz.y, objp->phys_info.desired_vel.xyz.y, flFrametime);
		lerp(&objp->phys_info.vel.xyz.z, objp->phys_info.vel.xyz.z, objp->phys_info.desired_vel.xyz.z, flFrametime);
	}
}

int asteroid_check_collision(object *pasteroid, object *other_obj, vec3d *hitpos, collision_info_struct *asteroid_hit_info, vec3d* hitnormal)
{
	if (!Asteroids_enabled) {
		return 0;
	}

	mc_info	mc;
	int		num, asteroid_subtype;

	Assert( pasteroid->type == OBJ_ASTEROID );

	num = pasteroid->instance;
	Assert( num >= 0 );

	Assert( Asteroids[num].objnum == OBJ_INDEX(pasteroid));
	asteroid_subtype = Asteroids[num].asteroid_subtype;

	// asteroid_hit_info NULL  --  asteroid-weapon collision
	if ( asteroid_hit_info == NULL ) {
		// asteroid weapon collision
		Assert( other_obj->type == OBJ_WEAPON );
		mc.model_instance_num = Asteroids[num].model_instance_num;
		mc.model_num = Asteroid_info[Asteroids[num].asteroid_type].subtypes[asteroid_subtype].model_number;	// Fill in the model to check
		mc.orient = &pasteroid->orient;					// The object's orient
		mc.pos = &pasteroid->pos;							// The object's position
		mc.p0 = &other_obj->last_pos;				// Point 1 of ray to check
		mc.p1 = &other_obj->pos;					// Point 2 of ray to check
		mc.flags = (MC_CHECK_MODEL);

		if (model_collide(&mc))
		{
			*hitpos = mc.hit_point_world;

			if (hitnormal)
			{
				vec3d normal;

				if (mc.model_instance_num >= 0)
					model_instance_local_to_global_dir(&normal, &mc.hit_normal, mc.model_instance_num, mc.hit_submodel, mc.orient);
				else
					model_local_to_global_dir(&normal, &mc.hit_normal, mc.model_num, mc.hit_submodel, mc.orient);

				*hitnormal = normal;
			}
		}

		return mc.num_hits;
	}

	// asteroid ship collision -- use asteroid_hit_info to calculate physics
	object *pship_obj = other_obj;
	Assert( pship_obj->type == OBJ_SHIP );

	object* heavy = asteroid_hit_info->heavy;
	object* lighter = asteroid_hit_info->light;
	object *heavy_obj = heavy;
	object *light_obj = lighter;

	vec3d zero, p0, p1;
	vm_vec_zero( &zero );
	vm_vec_sub( &p0, &lighter->last_pos, &heavy->last_pos );
	vm_vec_sub( &p1, &lighter->pos, &heavy->pos );

	mc.pos = &zero;								// The object's position
	mc.p0 = &p0;									// Point 1 of ray to check
	mc.p1 = &p1;									// Point 2 of ray to check

	// find the light object's position in the heavy object's reference frame at last frame and also in this frame.
	vec3d p0_temp, p0_rotated;
		
	// Collision detection from rotation enabled if at rotation is less than 30 degree in frame
	// This should account for all ships
	if ( (vm_vec_mag_squared( &heavy->phys_info.rotvel ) * flFrametime*flFrametime) < (PI*PI/36) ) {
		// collide_rotate calculate (1) start position and (2) relative velocity
		asteroid_hit_info->collide_rotate = true;
		vm_vec_rotate( &p0_temp, &p0, &heavy->last_orient );
		vm_vec_unrotate( &p0_rotated, &p0_temp, &heavy->orient );
		mc.p0 = &p0_rotated;				// Point 1 of ray to check
		vm_vec_sub( &asteroid_hit_info->light_rel_vel, &p1, &p0_rotated );
		vm_vec_scale( &asteroid_hit_info->light_rel_vel, 1/flFrametime );
		// HACK - this applies to big ships warping in/out of asteroid fields - not sure what it does
		if (vm_vec_mag(&asteroid_hit_info->light_rel_vel) > 300) {
			asteroid_hit_info->collide_rotate = false;
			vm_vec_sub( &asteroid_hit_info->light_rel_vel, &lighter->phys_info.vel, &heavy->phys_info.vel );
		}
	} else {
		asteroid_hit_info->collide_rotate = false;
		vm_vec_sub( &asteroid_hit_info->light_rel_vel, &lighter->phys_info.vel, &heavy->phys_info.vel );
	}

	int mc_ret_val = 0;

	if ( asteroid_hit_info->heavy == pship_obj ) {	// ship is heavier, so asteroid is sphere. Check sphere collision against ship poly model
		mc.model_instance_num = Ships[pship_obj->instance].model_instance_num;
		mc.model_num = Ship_info[Ships[pship_obj->instance].ship_info_index].model_num;		// Fill in the model to check
		mc.orient = &pship_obj->orient;								// The object's orient
		mc.radius = pasteroid->radius;
		mc.flags = (MC_CHECK_MODEL | MC_CHECK_SPHERELINE);

		// copy important data
		int orig_flags = mc.flags;  // make a copy of start end positions of sphere in big ship RF
		vec3d orig_p0 = *mc.p0;
		vec3d orig_p1 = *mc.p1;

		// first test against the sphere - if this fails then don't do any submodel tests
		mc.flags = MC_ONLY_SPHERE | MC_CHECK_SPHERELINE;

		if (model_collide(&mc)) {

			// Set earliest hit time
			asteroid_hit_info->hit_time = FLT_MAX;

			auto pmi = model_get_instance(Ships[heavy_obj->instance].model_instance_num);
			auto pm = model_get(pmi->model_num);

			// Do collision the cool new way
			if ( asteroid_hit_info->collide_rotate ) {
				// We collide with the sphere, find the list of moving submodels and test one at a time
				SCP_vector<int> submodel_vector;
				model_get_moving_submodel_list(submodel_vector, heavy_obj);

				// turn off all moving submodels, collide against only 1 at a time.
				mc.collision_checked.assign(pm->n_models, 0);
				for (auto submodel: submodel_vector) {
					mc.collision_checked[submodel] = true;
				}

				// Only check single submodel now, since children of moving submodels are handled as moving as well
				mc.flags = orig_flags | MC_SUBMODEL;

				// check each submodel in turn
				for (auto submodel: submodel_vector) {
					// turn on just one submodel for collision test
					mc.collision_checked[submodel] = false;

					// find the start and end positions of the sphere in submodel RF
					model_instance_global_to_local_point(&p0, &light_obj->last_pos, pm, pmi, submodel, &heavy_obj->last_orient, &heavy_obj->last_pos, true);
					model_instance_global_to_local_point(&p1, &light_obj->pos, pm, pmi, submodel, &heavy_obj->orient, &heavy_obj->pos);

					mc.p0 = &p0;
					mc.p1 = &p1;

					mc.orient = &vmd_identity_matrix;
					mc.submodel_num = submodel;

					if ( model_collide(&mc) ) {
						if ( mc.hit_dist < asteroid_hit_info->hit_time ) {
							mc_ret_val = 1;

							// set up asteroid_hit_info common
							set_hit_struct_info(asteroid_hit_info, &mc, true);

							// set up asteroid_hit_info for rotating submodel
							if (!asteroid_hit_info->edge_hit) {
								model_instance_local_to_global_dir(&asteroid_hit_info->collision_normal, &mc.hit_normal, pm, pmi, mc.hit_submodel, &heavy_obj->orient);
							}

							// find position in submodel RF of light object at collison
							vec3d int_light_pos, diff;
							vm_vec_sub(&diff, mc.p1, mc.p0);
							vm_vec_scale_add(&int_light_pos, mc.p0, &diff, mc.hit_dist);
							model_instance_local_to_global_point(&asteroid_hit_info->light_collision_cm_pos, &int_light_pos, pm, pmi, mc.hit_submodel, &heavy_obj->orient, &zero);
						}
					}
					// Don't look at this submodel again
					mc.collision_checked[submodel] = true;
				}

				// Clear collision_checked before base model pass so it auto-inits fresh
				mc.collision_checked.clear();

			}

			// Now complete base model collision checks that do not take into account rotating submodels.
			mc.flags = orig_flags;
			mc.p0 = &orig_p0;
			mc.p1 = &orig_p1;
			mc.orient = &heavy_obj->orient;

			// usual ship_ship collision test
			if ( model_collide(&mc) )	{
				// check if this is the earliest hit
				if (mc.hit_dist < asteroid_hit_info->hit_time) {
					mc_ret_val = 1;

					set_hit_struct_info(asteroid_hit_info, &mc, false);

					// get collision normal if not edge hit
					if (!asteroid_hit_info->edge_hit) {
						model_instance_local_to_global_dir(&asteroid_hit_info->collision_normal, &mc.hit_normal, pm, pmi, mc.hit_submodel, &heavy_obj->orient);
					}

					// find position in submodel RF of light object at collison
					vec3d diff;
					vm_vec_sub(&diff, mc.p1, mc.p0);
					vm_vec_scale_add(&asteroid_hit_info->light_collision_cm_pos, mc.p0, &diff, mc.hit_dist);

				}
			}
		}

	} else {
		// Asteroid is heavier obj
		mc.model_instance_num = Asteroids[num].model_instance_num;
		mc.model_num = Asteroid_info[Asteroids[num].asteroid_type].subtypes[asteroid_subtype].model_number;		// Fill in the model to check
		mc.orient = &pasteroid->orient;				// The object's orient
		mc.radius = model_get_core_radius(Ship_info[Ships[pship_obj->instance].ship_info_index].model_num);

		// check for collision between asteroid model and ship sphere
		mc.flags = (MC_CHECK_MODEL | MC_CHECK_SPHERELINE);

		mc_ret_val = model_collide(&mc);

		if (mc_ret_val) {
			set_hit_struct_info(asteroid_hit_info, &mc, false);

			// set normal if not edge hit
			if ( !asteroid_hit_info->edge_hit ) {
				vm_vec_unrotate(&asteroid_hit_info->collision_normal, &mc.hit_normal, &heavy->orient);
			}

			// find position in submodel RF of light object at collison
			vec3d diff;
			vm_vec_sub(&diff, mc.p1, mc.p0);
			vm_vec_scale_add(&asteroid_hit_info->light_collision_cm_pos, mc.p0, &diff, mc.hit_dist);

		}
	}

	// check if the hit point is beyond the clip plane if the ship is warping.
	if (mc_ret_val) {
		WarpEffect* warp_effect = nullptr;
		ship* shipp = &Ships[pship_obj->instance];

		// this is extremely confusing but mc.hit_point_world isn't actually in world coords
		// everything above was calculated relative to the heavy's position
		vec3d actual_world_hit_pos = mc.hit_point_world + heavy_obj->pos;
		if ((shipp->is_arriving()) && (shipp->warpin_effect != nullptr))
			warp_effect = shipp->warpin_effect;
		else if ((shipp->flags[Ship::Ship_Flags::Depart_warp]) && (shipp->warpout_effect != nullptr))
			warp_effect = shipp->warpout_effect;

		if (warp_effect != nullptr && point_is_clipped_by_warp(&actual_world_hit_pos, warp_effect))
			mc_ret_val = 0;
	}
	

	if ( mc_ret_val )	{

		// SET PHYSICS PARAMETERS
		// already have (hitpos - heavy) and light_cm_pos

		// get r_heavy and r_light
		asteroid_hit_info->r_heavy = asteroid_hit_info->hit_pos;
		vm_vec_sub(&asteroid_hit_info->r_light, &asteroid_hit_info->hit_pos, &asteroid_hit_info->light_collision_cm_pos);

		// set normal for edge hit
		if ( asteroid_hit_info->edge_hit ) {
			vm_vec_copy_normalize(&asteroid_hit_info->collision_normal, &asteroid_hit_info->r_light);
			vm_vec_negate(&asteroid_hit_info->collision_normal);
		}

		// get world hitpos
		vm_vec_add(hitpos, &asteroid_hit_info->heavy->pos, &asteroid_hit_info->r_heavy);

		return 1;
	} else {
		// no hit
		return 0;
	}
}

void asteroid_render(object * obj, model_draw_list *scene)
{
	if (Asteroids_enabled) {
		int			num;
		asteroid		*asp;
		
		num = obj->instance;

		Assert((num >= 0) && (num < MAX_ASTEROIDS));
		asp = &Asteroids[num];

		Assert( asp->flags & AF_USED );

		model_clear_instance( Asteroid_info[asp->asteroid_type].subtypes[asp->asteroid_subtype].model_number);

		model_render_params render_info;

		uint64_t render_flags = MR_IS_ASTEROID;

		if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_light])
			render_flags |= MR_NO_LIGHTING;

		if (asp->render_flags[Object::Raw_Pof_Flags::Glowmaps_disabled]) {
			render_flags |= MR_NO_GLOWMAPS;
		}

		if (asp->render_flags[Object::Raw_Pof_Flags::Draw_as_wireframe]) {
			render_flags |= MR_SHOW_OUTLINE_HTL | MR_NO_POLYS | MR_NO_TEXTURING;
			render_info.set_color(Wireframe_color);
		}

		if (asp->render_flags[Object::Raw_Pof_Flags::Render_full_detail]) {
			render_flags |= MR_FULL_DETAIL;
		}

		uint debug_flags = render_info.get_debug_flags();

		if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_diffuse]) {
			debug_flags |= MR_DEBUG_NO_DIFFUSE;
		}

		if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_glowmap]) {
			debug_flags |= MR_DEBUG_NO_GLOW;
		}

		if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_normalmap]) {
			debug_flags |= MR_DEBUG_NO_NORMAL;
		}

		if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_ambientmap]) {
			debug_flags |= MR_DEBUG_NO_AMBIENT;
		}

		if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_specmap]) {
			debug_flags |= MR_DEBUG_NO_SPEC;
		}

		if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_reflectmap]) {
			debug_flags |= MR_DEBUG_NO_REFLECT;
		}

		render_info.set_object_number( OBJ_INDEX(obj) );

		render_info.set_flags(render_flags);
		render_info.set_debug_flags(debug_flags);
		if (asp->model_instance_num >= 0)
			render_info.set_replacement_textures(model_get_instance(asp->model_instance_num)->texture_replace);

		model_render_queue(&render_info, scene, Asteroid_info[asp->asteroid_type].subtypes[asp->asteroid_subtype].model_number, &obj->orient, &obj->pos);	//	Replace MR_NORMAL with 0x07 for big yellow blobs
	}
}

/**
 * Create a normalized vector generally in the direction from *hitpos to other_obj->pos
 */
static void asc_get_relvec(vec3d *relvec, object *other_obj, vec3d *hitpos)
{
	vec3d	tvec, rand_vec;
	int		count = 0;

	vm_vec_normalized_dir(&tvec, &other_obj->pos, hitpos);

	//	Try up to three times to get a good vector.
	while (count++ < 3) {
		vm_vec_rand_vec_quick(&rand_vec);
		vm_vec_add(relvec, &tvec, &rand_vec);
		float mag = vm_vec_mag_quick(relvec);
		if ((mag > 0.2f) && (mag < 1.7f))
			break;
	}

	vm_vec_normalize_quick(relvec);
}

/**
 * Return multiplier on asteroid radius for fireball
 */
static float asteroid_get_fireball_scale_multiplier(int num)
{
	if (Asteroids[num].flags & AF_USED) {
		asteroid_info *asip = &Asteroid_info[Asteroids[num].asteroid_type];
		
		if (asip->fireball_radius_multiplier >= 0) {
			return asip->fireball_radius_multiplier;
		} else {
			switch(Asteroids[num].asteroid_type) {
			case ASTEROID_TYPE_LARGE:
				return 1.5f;
				break;

			default:
				return 1.0f;
				break;
			}
		}
	}

	Int3();	// this should not happen.  asteroid should be used.
	return 1.0f;
}


/**
 * Create asteroid explosion
 * @return expected time for explosion anim to last, in seconds
 */
static float asteroid_create_explosion(object *objp)
{
	int	fireball_objnum;
	float	explosion_life, fireball_scale_multiplier;
	asteroid_info *asip = &Asteroid_info[Asteroids[objp->instance].asteroid_type];

	int fireball_type = fireball_asteroid_explosion_type(asip);
	if (fireball_type < 0) {
		fireball_type = FIREBALL_ASTEROID;
	}

	if (fireball_type >= static_cast<int>(Fireball_info.size())) {
		Warning(LOCATION, "Invalid fireball type %i specified for an asteroid; only " SIZE_T_ARG " fireball types are defined.", fireball_type, Fireball_info.size());
		return 0;
	}

	fireball_scale_multiplier = asteroid_get_fireball_scale_multiplier(objp->instance);
	auto fireball_radius = objp->radius * fireball_scale_multiplier;

	if (Zero_radius_explosions_skip_fireballs && fl_near_zero(fireball_radius))
		fireball_objnum = -1;
	else
		fireball_objnum = fireball_create( &objp->pos, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(objp), fireball_radius, false, &objp->phys_info.vel );

	if ( fireball_objnum > -1 )	{
		explosion_life = fireball_lifeleft(&Objects[fireball_objnum]);
	} else {
		explosion_life = 0.0f;
	}

	return explosion_life;
}

/**
 * Play sound when asteroid explodes
 */
static void asteroid_explode_sound(object *objp, int type, int play_loud)
{
	gamesnd_id sound_index;
	float range_factor = 1.0f;		// how many times sound should traver farther than normal

	if (type % NUM_ASTEROID_SIZES <= 1)
	{
		sound_index = gamesnd_id(GameSounds::ASTEROID_EXPLODE_SMALL);
		range_factor = 5.0;
	}
	else
	{
		sound_index = gamesnd_id(GameSounds::ASTEROID_EXPLODE_LARGE);
		range_factor = 10.0f;
	}

	Assert(sound_index.isValid());

	if ( !play_loud ) {
		range_factor = 1.0f;
	}

	snd_play_3d( gamesnd_get_game_sound(sound_index), &objp->pos, &Eye_position, objp->radius, NULL, 0, 1.0f, SND_PRIORITY_MUST_PLAY, NULL, range_factor );
}

/**
 * Do the area effect for an asteroid exploding
 *
 * @param asteroid_objp	object pointer to asteroid causing explosion
 */
static void asteroid_do_area_effect(object *asteroid_objp)
{
	object			*ship_objp;
	float				damage, blast;
	ship_obj			*so;
	asteroid			*asp;
	asteroid_info	*asip;

	asp = &Asteroids[asteroid_objp->instance];
	asip = &Asteroid_info[asp->asteroid_type];

	if ( asip->damage <= 0 ) {		// do a quick out if there is no damage to apply
		return;
	}

	for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
		ship_objp = &Objects[so->objnum];
		if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
			continue;

		// don't blast no-collide or navbuoys
		if ( !ship_objp->flags[Object::Object_Flags::Collides] || ship_get_SIF(ship_objp->instance)[Ship::Info_Flags::Navbuoy] ) {
			continue;
		}

		if ( weapon_area_calc_damage(ship_objp, &asteroid_objp->pos, asip->inner_rad, asip->outer_rad, asip->blast, asip->damage, &blast, &damage, asip->outer_rad) == -1 )
			continue;

		ship_apply_global_damage(ship_objp, asteroid_objp, &asteroid_objp->pos, damage, asip->damage_type_idx);
		weapon_area_apply_blast(nullptr, ship_objp, &asteroid_objp->pos, blast, false);
	}	// end for
}

/**
 * Upon asteroid asteroid_obj being hit. Apply damage and maybe make it break into smaller asteroids.
 *
 * @param pasteroid_obj		pointer to asteroid object getting hit
 * @param other_obj		object that hit asteroid, can be NULL if asteroid hit by area effect
 * @param hitpos		world position asteroid was hit, can be NULL if hit by area effect
 * @param damage		amount of damage to apply to asteroid
 */
void asteroid_hit( object * pasteroid_obj, object * other_obj, vec3d * hitpos, float damage, vec3d* force )
{
	asteroid	*asp = &Asteroids[pasteroid_obj->instance];
	asteroid_info *asip = &Asteroid_info[Asteroids[pasteroid_obj->instance].asteroid_type];
	
	if (pasteroid_obj->flags[Object::Object_Flags::Should_be_dead]){
		return;
	}
	
	if ( MULTIPLAYER_MASTER ){
		send_asteroid_hit( pasteroid_obj, other_obj, hitpos, damage, force );
	}
	
	if (hitpos && force && The_mission.ai_profile->flags[AI::Profile_Flags::Whackable_asteroids]) {
		vec3d rel_hit_pos = *hitpos - pasteroid_obj->pos;
		physics_calculate_and_apply_whack(force, &rel_hit_pos, &pasteroid_obj->phys_info, &pasteroid_obj->orient, &pasteroid_obj->phys_info.I_body_inv);
		pasteroid_obj->phys_info.desired_vel = pasteroid_obj->phys_info.vel;
	}
	
	pasteroid_obj->hull_strength -= damage;
	
	if (pasteroid_obj->hull_strength < 0.0f) {
		if ( !asp->final_death_time.isValid() ) {
			int play_loud_collision = 0;
			
			float explosion_life = 1.f;
			int breakup_timestamp;
			
			Assertion(!asip->end_particles.isValid() || asip->breakup_delay.has_value(), "Asteroid %s has end particles but no breakup delay. Parsing should not have allowed this!", asip->name);

			if (asip->end_particles.isValid()) {
				auto source = particle::ParticleManager::get()->createSource(asip->end_particles);

				// Use the position since the asteroid is going to be invalid soon
				auto host = std::make_unique<EffectHostVector>(pasteroid_obj->pos, pasteroid_obj->orient, pasteroid_obj->phys_info.vel);
				host->setRadius(pasteroid_obj->radius);
				source->setHost(std::move(host));
				source->setNormal(pasteroid_obj->orient.vec.uvec);
				source->finishCreation();
			} else {
				explosion_life = asteroid_create_explosion(pasteroid_obj);
			}

			if (asip->breakup_delay.has_value()) {
				breakup_timestamp = fl2i(*asip->breakup_delay * MILLISECONDS_PER_SECOND);
			} else {
				breakup_timestamp = fl2i((explosion_life * MILLISECONDS_PER_SECOND) / 5.f);
			}

			asteroid_explode_sound(pasteroid_obj, asp->asteroid_type, play_loud_collision);
			asteroid_do_area_effect(pasteroid_obj);

			asp->final_death_time = _timestamp( breakup_timestamp );	// Wait till 20% of vclip time before breaking the asteroid up, or use the specified delay
			if ( hitpos ) {
				asp->death_hit_pos = *hitpos;
			} else {
				asp->death_hit_pos = pasteroid_obj->pos;
				// randomize hit pos a bit, otherwise we will get a NULL vector when trying to find direction to toss child asteroids
				vec3d rand_vec;
				vm_vec_rand_vec_quick(&rand_vec);
				vm_vec_add2(&asp->death_hit_pos, &rand_vec);
			}
		}
	} else if ( other_obj ) {
		if ( other_obj->type == OBJ_WEAPON ) {
			weapon_info *wip;
			wip = &Weapon_info[Weapons[other_obj->instance].weapon_info_index];
			// If the weapon didn't play any impact animation, play custom asteroid impact animation
			if (!wip->impact_weapon_expl_effect.isValid() && Asteroid_impact_explosion_ani.isValid()) {
				auto source = particle::ParticleManager::get()->createSource(Asteroid_impact_explosion_ani);
				source->setHost(std::make_unique<EffectHostVector>(*hitpos, vmd_identity_matrix, vmd_zero_vector));
				source->finishCreation();
			}
		}
	}

	// evaluate any relevant player scoring implications
	scoring_eval_hit(pasteroid_obj,other_obj);
}

/**
 * De-init asteroids, called from ::game_level_close()
 */
void asteroid_level_close()
{

	for (int i=0; i<MAX_ASTEROIDS; i++) {
		if (Asteroids[i].flags & AF_USED) {
			Asteroids[i].flags &= ~AF_USED;
			Assert(Asteroids[i].objnum >=0 && Asteroids[i].objnum < MAX_OBJECTS);
			Objects[Asteroids[i].objnum].flags.set(Object::Object_Flags::Should_be_dead);
		}
	}

	//when a level is closed, all models are cleared, so let's make sure that
	//is tracked for asteroids as well -Mjn
	for (size_t i = 0; i < Asteroid_info.size(); i++) {
		for (size_t j = 0; j < Asteroid_info[i].subtypes.size(); j++) {
			Asteroid_info[i].subtypes[j].model_number = -1;
		}
	}

	Asteroid_field.num_initial_asteroids=0;
}

DCF_BOOL2(asteroids, Asteroids_enabled, "enables or disables asteroids", "Usage: asteroids [bool]\nTurns asteroid system on/off.  If nothing passed, then toggles it.\n");


/**
 * Return the number of active asteroids
 */
int asteroid_count()
{
	return Num_asteroids;
}

/**
 * See if asteroid should split up.  
 * We delay splitting up to allow the explosion animation to play for a bit.
 */
static void asteroid_maybe_break_up(object *pasteroid_obj)
{
	asteroid *asp;
	asteroid_info *asip;

	asp = &Asteroids[pasteroid_obj->instance];
	asip = &Asteroid_info[asp->asteroid_type];

	if ( timestamp_elapsed(asp->final_death_time) ) {
		vec3d	relvec, vfh, tvec;
		bool skip = false;
		bool hooked = scripting::hooks::OnDeath->isActive();

		if (hooked) {
			skip = scripting::hooks::OnDeath->isOverride(scripting::hooks::ObjectDeathConditions{ pasteroid_obj },
				scripting::hook_param_list(scripting::hook_param("Self", 'o', pasteroid_obj)));
		}
		if (!skip)
		{
			pasteroid_obj->flags.set(Object::Object_Flags::Should_be_dead);

			// multiplayer clients won't go through the following code.  asteroid_sub_create will send
			// a create packet to the client in the above named function
			// if the second condition isn't true it's just debris, and debris doesn't break up
			if ( !MULTIPLAYER_CLIENT && (asp->asteroid_type <= ASTEROID_TYPE_LARGE)) {
				if (asip->split_info.empty()) {
					switch (asp->asteroid_type) {
						case ASTEROID_TYPE_SMALL:
							break;
						case ASTEROID_TYPE_MEDIUM:
							asc_get_relvec(&relvec, pasteroid_obj, &asp->death_hit_pos);
							asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_SMALL, &relvec);
						
							vm_vec_normalized_dir(&vfh, &pasteroid_obj->pos, &asp->death_hit_pos);
							vm_vec_copy_scale(&tvec, &vfh, 2.0f);
							vm_vec_sub2(&tvec, &relvec);
							asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_SMALL, &tvec);
							
							break;
						case ASTEROID_TYPE_LARGE:
							asc_get_relvec(&relvec, pasteroid_obj, &asp->death_hit_pos);
							asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_MEDIUM, &relvec);
						
							vm_vec_normalized_dir(&vfh, &pasteroid_obj->pos, &asp->death_hit_pos);
							vm_vec_copy_scale(&tvec, &vfh, 2.0f);
							vm_vec_sub2(&tvec, &relvec);
							asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_MEDIUM, &tvec);

							while (frand() > 0.6f) {
								vec3d	rvec, tvec2;
								vm_vec_rand_vec_quick(&rvec);
								vm_vec_scale_add(&tvec2, &vfh, &rvec, 0.75f);
								asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_SMALL, &tvec2);
							}
							break;

						default: // this isn't going to happen.. really
							break;
					}
				} else {
					SCP_vector<int> roids_to_create;
					SCP_vector<asteroid_split_info>::iterator split;
					for (split = asip->split_info.begin(); split != asip->split_info.end(); ++split) {
						int num_roids = split->min;
						int num_roids_var = split->max - split->min;

						if (num_roids_var > 0)
							num_roids += Random::next(num_roids_var);

						if (num_roids > 0)
							for (int i=0; i<num_roids; i++)
								roids_to_create.push_back(split->asteroid_type);
					}

					std::random_device rd;
					std::shuffle(roids_to_create.begin(), roids_to_create.end(), std::mt19937(rd()));

					size_t total_roids = roids_to_create.size();
					for (size_t i = 0; i < total_roids; i++) {
						vec3d dir_vec,final_vec;
						vec3d parent_vel,hit_rel_vec;

						// The roid directions are picked from the so-called
						// "golden section spiral" to prevent them from
						// clustering and thus clipping

						float inc = PI * (3.0f - sqrt(5.0f));
						float offset = 2.0f / total_roids;

						float y = i * offset - 1 + (offset / 2);
						float r = sqrt(1.0f - y*y);
						float phi = i * inc;

						dir_vec.xyz.x = cosf(phi)*r;
						dir_vec.xyz.y = sinf(phi)*r;
						dir_vec.xyz.z = y;

						// Randomize the direction a bit
						vec3d tempv = dir_vec;
						vm_vec_random_cone(&dir_vec, &tempv, (360.0f / total_roids / 2));

						// Make the roid inherit half of parent's directional movement
						if (!IS_VEC_NULL(&pasteroid_obj->phys_info.vel)) {
							vm_vec_copy_normalize(&parent_vel, &pasteroid_obj->phys_info.vel);
						} else {
							vm_vec_rand_vec_quick(&parent_vel);
						}
						vm_vec_scale(&parent_vel, 0.5f);

						// Make the hit position affect the direction, but only a little
						vm_vec_sub(&hit_rel_vec, &pasteroid_obj->pos, &asp->death_hit_pos);
						vm_vec_normalize(&hit_rel_vec);
						vm_vec_scale(&hit_rel_vec, 0.25f);

						vm_vec_avg3(&final_vec, &parent_vel, &hit_rel_vec, &dir_vec);
						vm_vec_normalize(&final_vec);

						asteroid_sub_create(pasteroid_obj, roids_to_create[i], &final_vec);
					}
				}
			}
			asp->final_death_time = TIMESTAMP::invalid();
		}
		if (hooked) {
			scripting::hooks::OnDeath->run(scripting::hooks::ObjectDeathConditions{ pasteroid_obj },
				scripting::hook_param_list(scripting::hook_param("Self", 'o', pasteroid_obj)));
		}
		if (scripting::hooks::OnAsteroidDeath->isActive()) {
			scripting::hooks::OnAsteroidDeath->run(
				scripting::hook_param_list(
					scripting::hook_param("Asteroid", 'o', pasteroid_obj),
					scripting::hook_param("Hitpos", 'o', asp->death_hit_pos)));
		}
	}
}

static void asteroid_test_collide(object *pasteroid_obj, object *pship_obj, mc_info *mc, bool lazy = false)
{
	float		asteroid_ray_dist;
	vec3d	asteroid_fvec, terminus;

	// See if ray from asteroid intersects bounding box of escort ship
	asteroid_ray_dist = vm_vec_mag_quick(&pasteroid_obj->phys_info.desired_vel) * ASTEROID_MIN_COLLIDE_TIME;
	asteroid_fvec = pasteroid_obj->phys_info.desired_vel;

	if(IS_VEC_NULL_SQ_SAFE(&asteroid_fvec)){
		terminus = pasteroid_obj->pos;
	} else {
		vm_vec_normalize(&asteroid_fvec);
		vm_vec_scale_add(&terminus, &pasteroid_obj->pos, &asteroid_fvec, asteroid_ray_dist);
	}

	Assert(pship_obj->type == OBJ_SHIP);

	mc->model_instance_num = Ships[pship_obj->instance].model_instance_num;
	mc->model_num = Ship_info[Ships[pship_obj->instance].ship_info_index].model_num;			// Fill in the model to check
	mc->orient = &pship_obj->orient;										// The object's orientation
	mc->pos = &pship_obj->pos;												// The object's position
	mc->p0 = &pasteroid_obj->pos;											// Point 1 of ray to check
	mc->p1 = &terminus;														// Point 2 of ray to check
	if (lazy) {
		mc->flags = MC_CHECK_MODEL | MC_ONLY_BOUND_BOX;
	} else {
		mc->flags = MC_CHECK_MODEL | MC_CHECK_SPHERELINE;
	}
	mc->radius = pasteroid_obj->radius;

	model_collide(mc);

	// PVS-Studio says that mc->p1 will become invalid... on the one hand, it will; on the other hand, we won't use it again; on the *other* other hand, we should keep things proper in case of future changes
	mc->p1 = NULL;
}

/**
 * Test if asteroid will collide with escort ship within ASTEROID_MIN_COLLIDE_TIME seconds
 *
 * @return !0 if the asteroid will collide with the escort
 */
static int asteroid_will_collide(object *pasteroid_obj, object *escort_objp)
{
	mc_info	mc;

	asteroid_test_collide(pasteroid_obj, escort_objp, &mc);

	if ( !mc.num_hits ) {
		return 0;
	}	

	return 1;
}

/**
 * Warn if asteroid on collision path with ship
 * @return !0 if we should warn about asteroid hitting ship, otherwise return 0
 */
static int asteroid_valid_ship_to_warn_collide(ship *shipp)
{
	if ( !(Ship_info[shipp->ship_info_index].is_big_or_huge()) ) {
		return 0;
	}

	if ( shipp->flags[Ship::Ship_Flags::Dying] || shipp->flags[Ship::Ship_Flags::Depart_warp] ) {
		return 0;
	}

	// Goober5000 used to be if teams were unequal and player was not traitor, but this works for allies not on your team
	if ( iff_x_attacks_y(Player_ship->team, shipp->team) ) {
		return 0;
	}

	return 1;
}

/**
 * See if asteroid will collide with a large ship on the escort list in the next
 * ASTEROID_MIN_COLLIDE_TIME seconds.
 */
static void asteroid_update_collide_flag(object *asteroid_objp)
{
	int		i, num_escorts, escort_objnum, will_collide=0;
	ship		*escort_shipp;
	asteroid	*asp;

	asp = &Asteroids[asteroid_objp->instance];
	asp->collide_objnum = -1;
	asp->collide_objsig = -1;

	// multiplayer dogfight
	if(MULTI_DOGFIGHT){
		return;
	}

	num_escorts = hud_escort_num_ships_on_list();

	if ( num_escorts <= 0 ) {
		return;
	}

	for ( i = 0; i < num_escorts; i++ ) {
		escort_objnum = hud_escort_return_objnum(i);
		if ( escort_objnum >= 0 ) {
			escort_shipp = &Ships[Objects[escort_objnum].instance];
			if ( asteroid_valid_ship_to_warn_collide(escort_shipp) ) {
				will_collide = asteroid_will_collide(asteroid_objp, &Objects[escort_objnum]);
				if ( will_collide ) {
					asp->collide_objnum = escort_objnum;
					asp->collide_objsig = Objects[escort_objnum].signature;
				}
			}
		}
	}
}

/**
 * Ensure that the collide objnum for the asteroid is still valid
 */
static void asteroid_verify_collide_objnum(asteroid *asp)
{
	if ( asp->collide_objnum >= 0 ) {
		if ( Objects[asp->collide_objnum].signature != asp->collide_objsig ) {
			asp->collide_objnum = -1;
			asp->collide_objsig = -1;
		}
	}
}

void asteroid_process_post(object * obj)
{
	if (Asteroids_enabled) {
		int num;
		num = obj->instance;
		
		asteroid	*asp = &Asteroids[num];

		//Passive fields might wrap if there's gravity so do
		//this for all fields now-Mjn
		if ( timestamp_elapsed(asp->check_for_wrap) ) {
			asteroid_maybe_reposition(obj, &Asteroid_field);
			asp->check_for_wrap = _timestamp(ASTEROID_CHECK_WRAP_TIMESTAMP);
		}

		asteroid_verify_collide_objnum(asp);

		if ( timestamp_elapsed(asp->check_for_collide) ) {
			asteroid_update_collide_flag(obj);
			asp->check_for_collide = _timestamp(ASTEROID_UPDATE_COLLIDE_TIMESTAMP);
		}

		asteroid_maybe_break_up(obj);
	}
}

/**
 * Find the object number of the object the asteroid is about to impact
 * @return the object number that the asteroid is about to impact
 */
int asteroid_collide_objnum(object *asteroid_objp)
{
	return Asteroids[asteroid_objp->instance].collide_objnum;
}

/**
 * Find the time until asteroid will collide with object
 * @return the time until the asteroid will impact its collide_objnum
 */
float asteroid_time_to_impact(object *asteroid_objp)
{
	float		time=-1.0f, total_dist, speed;
	asteroid	*asp;
	mc_info	mc;

	asp = &Asteroids[asteroid_objp->instance];

	if ( asp->collide_objnum < 0 ) {
		return time;
	}
	
	asteroid_test_collide(asteroid_objp, &Objects[asp->collide_objnum], &mc, true);

	if ( mc.num_hits ) {
		total_dist = vm_vec_dist(&mc.hit_point_world, &asteroid_objp->pos) - asteroid_objp->radius;
		if ( total_dist < 0 ) {
			total_dist = 0.0f;
		}
		speed = vm_vec_mag(&asteroid_objp->phys_info.vel);
		time = total_dist/speed;
	}	

	return time;
}

static SCP_string setup_display_name(SCP_string name)
{

	size_t split = std::string::npos;
	char thisSpecies[NAME_LENGTH];

	for (auto& species : Species_info) {
		if (name.compare(0, strlen(species.species_name), species.species_name) == 0) {
			split = strlen(species.species_name);
			strcpy_s(thisSpecies, species.species_name);
			break;
		}
	}

	if (split == std::string::npos)
		return XSTR("Asteroid", 431);

	char newName[NAME_LENGTH];
	strcpy_s(newName, thisSpecies);
	strcat(newName, " ");
	strcat(newName, XSTR("debris", 348));
	return newName;

}

// Temporary list used to grab parsed data. After verification, the list is cleared -Mjn
static SCP_vector<asteroid_info> asteroid_list;

// Gets the asteroid pointer for asteroid_list NOT Asteroid_info. Use only during parsing! - Mjn
static asteroid_info* get_asteroid_pointer(const char* asteroid_name)
{
	for (int i = 0; i < (int)asteroid_list.size(); i++) {
		if (!stricmp(asteroid_name, asteroid_list[i].name)) {
			return &asteroid_list[i];
		}
	}

	// Didn't find anything.
	return nullptr;
}

/**
 * Read in a single asteroid section from asteroid.tbl
 */
static void asteroid_parse_section()
{
	bool create_if_not_found = true;
	asteroid_info asteroid_t;
	
	required_string("$Name:");
	stuff_string(asteroid_t.name, F_NAME, NAME_LENGTH);

	if (optional_string("+nocreate")) {
		if (!Parsing_modular_table) {
			Warning(LOCATION, "+nocreate flag used for asteroid in non-modular table\n");
		}
		create_if_not_found = false;
	}

	// Does this asteroid exist already?
	// If so, load this new info into it
	asteroid_info* asteroid_p = get_asteroid_pointer(asteroid_t.name);

	if (asteroid_p != nullptr) {
		if (!Parsing_modular_table) {
			error_display(1,
				"Error:  Asteroid %s already exists.  All asteroid names must be unique.",
				asteroid_t.name);
		}
	} else {
		// Don't create asteroid if it has +nocreate and is in a modular table.
		if (!create_if_not_found && Parsing_modular_table) {
			if (!skip_to_start_of_string_either("$Name:", "#end")) {
				error_display(1, "Missing [#end] or [$Name] after asteroid %s", asteroid_t.name);
			}
			return;
		}
		asteroid_list.push_back(std::move(asteroid_t));
		asteroid_p = &asteroid_list[asteroid_list.size() - 1];

	}

	if (optional_string("$Display Name:")) {
		stuff_string(asteroid_p->display_name, F_NAME);
	} 
	
	// setup display name only if the string is empty - Mjn
	if (asteroid_p->display_name.empty()) {
		asteroid_p->display_name = setup_display_name(asteroid_p->name);
	}

	if (optional_string("$Type:")) {
		stuff_int(&asteroid_p->type);
	}

	switch (asteroid_p->type) {
	case ASTEROID_TYPE_SMALL:
	case ASTEROID_TYPE_MEDIUM:
	case ASTEROID_TYPE_LARGE:
	case ASTEROID_TYPE_DEBRIS:
		break;
	default:
		error_display(0, "Unknown asteroid type [%i] found for asteroid %s. Assuming debris", asteroid_p->type, asteroid_p->name);
		asteroid_p->type = ASTEROID_TYPE_DEBRIS;
	}

	auto insert_or_replace_subtype_pof = [](SCP_vector<asteroid_subtype_info>& subtypes, const asteroid_subtype_info& new_subtype) {
		for (auto& subtype : subtypes) {
			if (lcase_equal(subtype.type_name, new_subtype.type_name)) {
				strcpy_s(subtype.pof_filename, new_subtype.pof_filename);
				return;
			}
		}
		subtypes.push_back(new_subtype);
	};

	if (optional_string("$POF file1:")) {
		asteroid_subtype_info thisType;

		thisType.type_name = "Brown";
		
		stuff_string(thisType.pof_filename, F_NAME, MAX_FILENAME_LEN);

		thisType.model_number = -1;

		insert_or_replace_subtype_pof(asteroid_p->subtypes, thisType);
	}

	if (optional_string("$POF file2:")) {
		asteroid_subtype_info thisType;

		thisType.type_name = "Blue";
		stuff_string(thisType.pof_filename, F_NAME, MAX_FILENAME_LEN);

		thisType.model_number = -1;

		insert_or_replace_subtype_pof(asteroid_p->subtypes, thisType);
	}

	if (optional_string("$POF file3:")) {
		asteroid_subtype_info thisType;

		thisType.type_name = "Orange";
		stuff_string(thisType.pof_filename, F_NAME, MAX_FILENAME_LEN);

		thisType.model_number = -1;

		insert_or_replace_subtype_pof(asteroid_p->subtypes, thisType);
	}

	// For asteroid types we can have unlimited subtypes
	if (asteroid_p->type != ASTEROID_TYPE_DEBRIS) {
		while (optional_string("$Subtype:")) {
			asteroid_subtype_info thisType;

			stuff_string(thisType.type_name, F_NAME);

			required_string("+POF file:");
			stuff_string(thisType.pof_filename, F_NAME, MAX_FILENAME_LEN);

			thisType.model_number = -1;

			bool exists = false;
			for (const auto& type : asteroid_p->subtypes) {
				if (type.type_name == thisType.type_name) {
					exists = true;
				}
			}

			if (exists) {
				error_display(0,
					"Asteroid subtype %s already exists for asteroid %s",
					thisType.type_name.c_str(),
					asteroid_p->name);
			} else {
				asteroid_p->subtypes.push_back(std::move(thisType));
			}
		}
	}

	if (optional_string("$Detail distance:")) {
		asteroid_p->num_detail_levels = sz2i(stuff_int_list(asteroid_p->detail_distance, MAX_ASTEROID_DETAIL_LEVELS, ParseLookupType::RAW_INTEGER_TYPE));
	}

	if (optional_string("$Max Speed:")) {
		stuff_float(&asteroid_p->max_speed);
	}

	if (optional_string("$Rotational Velocity Multiplier:")) {
		stuff_float(&asteroid_p->rotational_vel_multiplier);
	}

	if(optional_string("$Damage Type:")) {
		char buf[NAME_LENGTH];
		stuff_string(buf, F_NAME, NAME_LENGTH);
		asteroid_p->damage_type_idx_sav = damage_type_add(buf);
		asteroid_p->damage_type_idx = asteroid_p->damage_type_idx_sav;
	}

	if (optional_string("$Explosion Effect:")) {
		asteroid_p->end_particles = particle::util::parseEffect(asteroid_p->name);
	} else {
		if(optional_string("$Explosion Animations:")){
			stuff_fireball_index_list(asteroid_p->explosion_bitmap_anims, asteroid_p->name);
		}
		
		if (optional_string("$Explosion Radius Mult:")) {
				stuff_float(&asteroid_p->fireball_radius_multiplier);
		}
	}

	if (optional_string("$Breakup Delay:")) {
		stuff_float(&asteroid_p->breakup_delay.emplace());
	}

	if (asteroid_p->end_particles.isValid() && !asteroid_p->breakup_delay.has_value()) {
		error_display(0, "Asteroid %s has an explosion effect but no breakup delay!", asteroid_p->name);
	}

	if (optional_string("$Expl inner rad:")){
		stuff_float(&asteroid_p->inner_rad);
	}

	if (optional_string("$Expl outer rad:")) {
		stuff_float(&asteroid_p->outer_rad);
	}

	if (optional_string("$Expl damage:")){
		stuff_float(&asteroid_p->damage);
	}

	if (optional_string("$Expl blast:")){
		stuff_float(&asteroid_p->blast);
	}

	if (optional_string("$Hitpoints:")){
		stuff_float(&asteroid_p->initial_asteroid_strength);
	}

	while(optional_string("$Split:")) {
		int split_index;

		stuff_int(&split_index);

		if (split_index >=0 && split_index < (int)asteroid_list.size()) {
			asteroid_split_info new_split;

			new_split.asteroid_type = split_index;
			strcpy(new_split.name, asteroid_list[split_index].name);

			if(optional_string("+Min:"))
				stuff_int(&new_split.min);
			else
				new_split.min = 0;

			if(optional_string("+Max:"))
				stuff_int(&new_split.max);
			else
				new_split.max = 0;

			asteroid_p->split_info.push_back(new_split);
		} else
			Warning(LOCATION, "Invalid asteroid reference %i used for $Split in asteroids table, ignoring.", split_index);
	}

	// provide a way to define splits by name instead of index, verified after all parsing is done - Mjn
	while (optional_string("$Split name:")) {
		char split_name[NAME_LENGTH];

		stuff_string(split_name, F_NAME, NAME_LENGTH);

		asteroid_split_info new_split;

		new_split.asteroid_type = -1;
		strcpy(new_split.name, split_name);

		if (optional_string("+Min:"))
			stuff_int(&new_split.min);
		else
			new_split.min = 0;

		if (optional_string("+Max:"))
			stuff_int(&new_split.max);
		else
			new_split.max = 0;

		asteroid_p->split_info.push_back(new_split);
	}

	if (optional_string("$Spawn Weight:")) {
		if (asteroid_p->type == ASTEROID_TYPE_DEBRIS) {
			stuff_float(&asteroid_p->spawn_weight);
			if (asteroid_p->spawn_weight <= 0.0f) {
				Warning(LOCATION, "Spawn weight for asteroid '%s' must be greater than 0", asteroid_p->name);
				asteroid_p->spawn_weight = 1.0f;
			}
		} else {
			Warning(LOCATION, "Spawn weight defined for non-debris type asteroid '%s', but can only be defined for debris types.", asteroid_p->name);
		}
	}

	if (optional_string("$Gravity Const:")) {
		stuff_float(&asteroid_p->gravity_const);
	}
}

/**
Read in data from asteroid.tbl into Asteroid_info[] array.

After this function is complete, the Asteroid_info[] array 
will have at least 3 generic asteroids plus whatever else 
special ones were added in the table.  The generic asteroids are 
a set of small, medium, and large asteroids in that exact order.

Note that by saying "asteroid" this code is talking about
the asteroids and the debris that make up asteroid fields
and debris fields. Which means that these debris have nothing
to do with the debris of ships that explode, however these
are the same debris and asteroids that get flung at a ship
that is being protected.
*/
static void asteroid_parse_tbl(const char* filename)
{
	
	try
	{
		read_file_text(filename, CF_TYPE_TABLES);
		reset_parse();

		required_string("#Asteroid Types");

		// parse each asteroid
		while (required_string_either("#End", "$Name:"))
		{

			asteroid_parse_section();

		}

		required_string("#End");

		if (optional_string("$Impact Explosion Effect:")) {
			Asteroid_impact_explosion_ani = particle::util::parseEffect();
		}
		else {
			char impact_ani_file[MAX_FILENAME_LEN] = {0};
			float Asteroid_impact_explosion_radius = 0.0f;
			int num_frames;

			if (optional_string("$Impact Explosion:")) {
				stuff_string(impact_ani_file, F_NAME, MAX_FILENAME_LEN);
			}
			if (optional_string("$Impact Explosion Radius:")) {
				stuff_float(&Asteroid_impact_explosion_radius);
			}

			if(VALID_FNAME(impact_ani_file)) {
				Asteroid_impact_explosion_ani = particle::ParticleManager::get()->addEffect(particle::ParticleEffect(
						"", //Name
						::util::UniformFloatRange(1.f), //Particle num
						particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission
						::util::UniformFloatRange(), //No duration
						::util::UniformFloatRange(-1.f), //Single particle only
						particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction
						::util::UniformFloatRange(), //Velocity Inherit
						false, //Velocity Inherit absolute?
						nullptr, //Velocity volume
						::util::UniformFloatRange(), //Velocity volume multiplier
						particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling
						std::nullopt, //Orientation-based velocity
						std::nullopt, //Position-based velocity
						nullptr, //Position volume
						particle::ParticleEffectHandle::invalid(), //Trail
						1.f, //Chance
						false, //Affected by detail
						-1.f, //Culling range multiplier
						false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X
						false, //Don't reverse animation
						false, //parent local
						false, //ignore velocity inherit if parented
						false, //position velocity inherit absolute?
						std::nullopt, //Local velocity offset
						std::nullopt, //Local offset
						::util::UniformFloatRange(-1.f), //Lifetime
						::util::UniformFloatRange(Asteroid_impact_explosion_radius), //Radius
						bm_load_animation(impact_ani_file, &num_frames, nullptr, nullptr, nullptr, true))); //Bitmap
			}
		}

		if (optional_string("$Briefing Icon Closeup Model:")) {
			stuff_string(Asteroid_icon_closeup_model, F_NAME, NAME_LENGTH);
		}

		if (optional_string("$Briefing Icon Closeup Position:")) {
			stuff_vec3d(&Asteroid_icon_closeup_position);
		}

		if (optional_string("$Briefing Icon Closeup Zoom:")) {
			stuff_float(&Asteroid_icon_closeup_zoom);
		}

	}
	catch (const parse::ParseException& e)
	{
		mprintf(("TABLES: Unable to parse '%s'!  Error message = %s.\n", filename, e.what()));
		return;
	}
}

// Gets the index of an asteroid by name from Asteroid_info
int get_asteroid_index(const char* asteroid_name)
{
	for (int i = 0; i < static_cast<int>(Asteroid_info.size()); i++) {
		if (!stricmp(asteroid_name, Asteroid_info[i].name)) {
			return i;
		}
	}

	return -1;
}

// Returns the list of unique asteroid subtype names.
// List is cached after the first call since Asteroid_info cannot change during an engine instance.
const SCP_vector<SCP_string>& get_list_valid_asteroid_subtypes()
{
	static SCP_vector<SCP_string> list;

	if (list.empty()) {
		for (const auto& this_asteroid : Asteroid_info) {
			if (this_asteroid.type != ASTEROID_TYPE_DEBRIS) {
				for (const auto& subtype : this_asteroid.subtypes) {
					// Only add unique names
					if (std::find(list.begin(), list.end(), subtype.type_name) == list.end()) {
						list.push_back(subtype.type_name);
					}
				}
			}
		}
	}

	return list;
}


static void verify_asteroid_splits() 
{
	
	for (int i = 0; i < (int)Asteroid_info.size(); i++) {
		SCP_vector<asteroid_split_info> splits_t;

		for (int j = 0; j < (int)Asteroid_info[i].split_info.size(); j++) {
			int asteroid_index = get_asteroid_index(Asteroid_info[i].split_info[j].name);
			if (asteroid_index >= 0) {
				Asteroid_info[i].split_info[j].asteroid_type = asteroid_index;
				splits_t.push_back(Asteroid_info[i].split_info[j]);
			} else {
				Warning(LOCATION,
					"Unknown asteroid %s defined as split type for asteroid %s, removing!",
					Asteroid_info[i].split_info[j].name,
					Asteroid_info[i].name);
			}
		}

		// Replace splits with only valid splits
		Asteroid_info[i].split_info = std::move(splits_t);
	}
}

static void verify_asteroid_list()
{
	SCP_string asteroid_size[NUM_ASTEROID_SIZES] = {"Small", "Medium", "Large"};
	float asteroid_spawn_weight[NUM_ASTEROID_SIZES] = {SMALL_DEBRIS_WEIGHT, MEDIUM_DEBRIS_WEIGHT, LARGE_DEBRIS_WEIGHT};
	
	// Set the three asteroid sizes
	for (int i = 0; i < NUM_ASTEROID_SIZES; i++) {

		bool found = false;

		for (int j = 0; j < (int)asteroid_list.size(); j++) {
			if (asteroid_list[j].type == i) {
				mprintf(("%s asteroid parsed with name %s\n", asteroid_size[i].c_str(), asteroid_list[j].name));
				asteroid_list[j].spawn_weight = asteroid_spawn_weight[i]; //Be absolutely sure we're using the right spawn weight for asteroids
				Asteroid_info.push_back(asteroid_list[j]);
				asteroid_list.erase(asteroid_list.begin() + j);
				found = true;
				break;
			}
		}

		if (found)
			continue;

		if (asteroid_list.empty()) {
			mprintf(("%s asteroid not found and no fallback available in asteroid_list.\n", asteroid_size[i].c_str()));
			continue;
		}

		//Left this as a log print instead of a Warning because of retail-Mjn
		mprintf(("%s asteroid not found. Using asteroid %s\n", asteroid_size[i].c_str(), asteroid_list[0].name));
		asteroid_list[0].type = i;
		asteroid_list[0].spawn_weight = asteroid_spawn_weight[i]; //Be absolutely sure we're using the right spawn weight for asteroids
		Asteroid_info.push_back(asteroid_list[0]);
		asteroid_list.erase(asteroid_list.begin());
		continue;

	}

	// Rest should be debris type
	for (int i = 0; i < (int)asteroid_list.size(); i++) {

		// Remove all subtypes except the first one
		asteroid_list[i].subtypes.resize(1);
		// Make sure the first subtype is set as debris
		asteroid_list[i].subtypes[0].type_name = "Debris";

		if (asteroid_list[i].type < 0) {
			Asteroid_info.push_back(asteroid_list[i]);
		} else {
			Warning(LOCATION, "Additional %s asteroid type found with name %s. Setting as debris type instead!", asteroid_size[asteroid_list[i].type].c_str(), asteroid_list[i].name);
			asteroid_list[i].type = ASTEROID_TYPE_DEBRIS;
			Asteroid_info.push_back(asteroid_list[i]);
		}
	}

	asteroid_list.clear();

	//Check that we have the appropriate POF files-Mjn
	for (int i = 0; i < (int)Asteroid_info.size(); i++) {
		if (Asteroid_info[i].subtypes.size() == 0) {
			Error(LOCATION, "Asteroid %s needs at least one subtype!", Asteroid_info[i].name);
		}
	}
}

/**
 * Called once, at game start.  Do any one-time initializations here
 */
void asteroid_init()
{
	Asteroid_impact_explosion_ani = particle::ParticleEffectHandle::invalid();
	Asteroid_icon_closeup_model[0] = '\0';
	vm_vec_make(&Asteroid_icon_closeup_position, 0.0f, 0.0f, -334.0f);	// magic numbers from retail
	Asteroid_icon_closeup_zoom = 0.5f;									// magic number from retail

	// first parse the default table
	asteroid_parse_tbl("asteroid.tbl");

	// parse any modular tables
	parse_modular_table("*-ast.tbm", asteroid_parse_tbl);

	//No asteroids defined. Bail!
	if (asteroid_list.empty()) {
		mprintf(("No asteroids defined in asteroid.tbl or any modular tables. Asteroid fields will not function.\n"));
		return;
	}

	// now verify the asteroids were found and put them in the correct order
	verify_asteroid_list();

	// now that Asteroid_info is filled in we can verify the asteroid splits and set their indecies
	verify_asteroid_splits();

	if (!Asteroid_impact_explosion_ani.isValid()) {
		// this will always be missing on a standalone
		if ( !Is_standalone ) {
			Error(LOCATION, "Missing valid asteroid impact explosion definition in asteroid.tbl!");
		}
	}

	if (Asteroid_icon_closeup_model[0] == '\0')
		strcpy_s(Asteroid_icon_closeup_model, Asteroid_info[ASTEROID_TYPE_LARGE].subtypes[0].pof_filename); // magic file from retail
	
}

/**
 * Pick object to throw asteroids at.
 * Pick any capital or big ship inside the bounds of the asteroid field.
 */
static int set_asteroid_throw_objnum()
{
	if (Asteroid_field.num_initial_asteroids < 1)
		return -1;

	ship_obj	*so;
	object	*ship_objp;

	for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
		ship_objp = &Objects[so->objnum];
		if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
			continue;

		if (Ship_info[Ships[ship_objp->instance].ship_info_index].is_big_or_huge()) {
			if (asteroid_is_ship_inside_field(&Asteroid_field, &ship_objp->pos, ship_objp->radius))
				return so->objnum;
		}
	}
	return -1;

}

void asteroid_frame()
{
	if (Num_asteroids < 1)
		return;

	// Only throw if active field
	if (Asteroid_field.field_type == FT_PASSIVE) {
		return;
	}

	// If no asteroid types are defined for the field, abort.
	if (Asteroid_field.field_asteroid_type.empty()) {
		return;
	}

	// If there are no explicit targets, fall back to default retail targeting behavior
	if (Asteroid_field.target_names.empty()) {
		int objnum = set_asteroid_throw_objnum();
		if (Asteroid_targets.empty() || Asteroid_targets[0].objnum != objnum) {
			Asteroid_targets.clear();
			if (objnum >= 0)
				asteroid_add_target(&Objects[objnum]);
		}
	} 

	sanitize_asteroid_targets_list();

	maybe_throw_asteroid();
}

extern bool Extra_target_info;

/**
 * Draw brackets around on-screen asteroids that are about to collide, otherwise draw an offscreen indicator
 */
void asteroid_show_brackets()
{
	vertex	asteroid_vertex;
	object	*asteroid_objp, *player_target;
	asteroid	*asp;

	Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "asteroid_show_brackets() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");

	// get pointer to player target, so we don't need to take OBJ_INDEX() of asteroid_objp to compare to target_objnum
	if ( Player_ai->target_objnum >= 0 ) {
		player_target = &Objects[Player_ai->target_objnum];
	} else {
		player_target = NULL;
	}

	for (auto aop: list_range(&Asteroid_obj_list)) {
		asteroid_objp = &Objects[aop->objnum];
		if (asteroid_objp->flags[Object::Object_Flags::Should_be_dead])
			continue;
		if (asteroid_objp->type != OBJ_ASTEROID ) {
			continue;
		}

		asp = &Asteroids[asteroid_objp->instance];

		if ( asp->collide_objnum < 0 ) {
			continue;
		}

		if ( asteroid_objp == player_target ) {
			continue;
		}

		g3_rotate_vertex(&asteroid_vertex,&asteroid_objp->pos);
		g3_project_vertex(&asteroid_vertex);

		if ( Extra_target_info ) {
			hud_target_add_display_list(asteroid_objp, &asteroid_vertex, &asteroid_objp->pos, 0, NULL, NULL, TARGET_DISPLAY_DIST | TARGET_DISPLAY_LEAD);
		} else {
			hud_target_add_display_list(asteroid_objp, &asteroid_vertex, &asteroid_objp->pos, 0, NULL, NULL, TARGET_DISPLAY_DIST);
		}
	}
}

/**
 * Target the closest danger asteroid to the player
 */
void asteroid_target_closest_danger()
{
	object	*asteroid_objp, *closest_asteroid_objp = NULL;
	asteroid	*asp;
	float		dist, closest_dist = 999999.0f;

	Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "asteroid_target_closest_danger() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");

	for (auto aop: list_range(&Asteroid_obj_list)) {
		asteroid_objp = &Objects[aop->objnum];
		if (asteroid_objp->flags[Object::Object_Flags::Should_be_dead])
			continue;
		if (asteroid_objp->type != OBJ_ASTEROID ) {
			continue;
		}

		asp = &Asteroids[asteroid_objp->instance];

		if ( asp->collide_objnum < 0 ) {
			continue;
		}

		dist = vm_vec_dist_quick(&Player_obj->pos, &asteroid_objp->pos);

		if ( dist < closest_dist ) {
			closest_dist = dist;
			closest_asteroid_objp = asteroid_objp;
		}
	}

	if ( closest_asteroid_objp ) {
		set_target_objnum( Player_ai, OBJ_INDEX(closest_asteroid_objp) );
	}
}

void asteroid_page_in()
{
	if (Asteroid_info.empty())
		return;

	if (Asteroid_field.num_initial_asteroids > 0 ) {

		nprintf(( "Paging", "Paging in asteroids\n" ));

		size_t num_debris_types = NUM_ASTEROID_SIZES;
		if (Asteroid_field.debris_genre == DG_DEBRIS) {
			num_debris_types = Asteroid_field.field_debris_type.size();
		}


		// for asteroids this will loop over all three possible sizes
		// for debris this will loop over all selected types
		for (size_t i = 0; i < num_debris_types; i++) {
			asteroid_info	*asip;

			if (Asteroid_field.debris_genre == DG_ASTEROID) {
				// asteroid
				Assertion(i < NUM_ASTEROID_SIZES, "Got invalid call to load non-existent asteroid size!");
				asip = &Asteroid_info[i];
			} else {
				// debris - always full until empty
				if (Asteroid_field.field_debris_type[i] != -1) {
					asip = &Asteroid_info[Asteroid_field.field_debris_type[i]];
				} else {
					break;
				}
			}


			for (size_t k = 0; k < asip->subtypes.size(); k++) {

				// DEBRIS FIELDS - always use subtype 0
				if (Asteroid_field.debris_genre == DG_DEBRIS) {
					if (k > 0) {
						break;
					}
				} else {
					// ASTEROID FIELDS - use subtype k, if active
					bool active = false;
					for (const auto& active_type : Asteroid_field.field_asteroid_type) {
						if (active_type == asip->subtypes[k].type_name) {
							active = true;
						}
					}

					if (!active) {
						continue;
					}
				}

				// model is already paged in
				if (asip->subtypes[k].model_number < 0)
					continue;

				const auto mp = model_get(asip->subtypes[k].model_number);

				// Page in textures
				for (int j = 0; j < mp->n_textures; j++) {
					mp->maps[j].PageIn();			
				}

			}
		} 
	}
}
