Files
fs2open.github.com/code/prop/prop.cpp
T
Goober5000 1832fa0397 fix a FRED2 crash
Fix a crash caused in part by some missing UI calls in FRED2 and in part by subtle object life cycle issues.  Depending on whether FRED receives the focus during mission load (which can happen if the autosave dialog appears) it could access props that were in the process of being cleaned up but were still referenced.  This isn't specifically a props issue; it could happen to any FSO object stored in a SCP_vector, since the vector can be cleared.

The quick mission init is to prevent a whole bunch of unnecessary and risky processing during a transient loading state, but it has the nice side effect of making mission list loading noticeably faster.
2026-02-19 00:29:53 -05:00

1132 lines
34 KiB
C++

#include "prop.h"
#include "asteroid/asteroid.h"
#include "debris/debris.h"
#include "freespace.h"
#include "model/model.h"
#include "model/modelreplace.h"
#include "network/multiutil.h"
#include "parse/parselo.h"
#include "prop/prop_flags.h"
#include "render/3d.h"
#include "ship/shipfx.h"
#include "object/objcollide.h"
#include "tracing/Monitor.h"
MONITOR(NumPropsRend)
bool Props_inited = false;
SCP_vector<prop_info> Prop_info;
SCP_vector<std::optional<prop>> Props;
SCP_vector<prop_category> Prop_categories;
static SCP_vector<SCP_string> Removed_props;
flag_def_list_new<Prop::Info_Flags> Prop_flags[] = {
{ "no_collide", Prop::Info_Flags::No_collide, true, false },
{ "no_fred", Prop::Info_Flags::No_fred, true, false },
{ "no lighting", Prop::Info_Flags::No_lighting, true, false }
//{ "no impact debris", Prop::Info_Flags::No_impact_debris, true, false },
};
//const size_t Num_prop_flags = sizeof(Prop_flags) / sizeof(flag_def_list_new<Prop::Info_Flags>);
/**
* Return the index of Prop_info[].name that is *token.
*/
int prop_info_lookup(const char* token)
{
Assertion(token != nullptr, "NULL token passed to prop_info_lookup");
for (auto it = Prop_info.cbegin(); it != Prop_info.cend(); ++it)
if (!stricmp(token, it->name.c_str()))
return (int)std::distance(Prop_info.cbegin(), it);
return -1;
}
int prop_name_lookup(const char *name)
{
Assertion(name != nullptr, "NULL name passed to prop_name_lookup");
for (size_t i=0; i < Props.size(); i++){
auto p = Props[i] ? &Props[i].value() : nullptr;
if (p == nullptr) {
continue;
}
if (p->objnum >= 0){
if (Objects[p->objnum].type == OBJ_PROP) {
if (!stricmp(name, p->prop_name)) {
return static_cast<int>(i);
}
}
}
}
// couldn't find it
return -1;
}
int find_prop_empty_slot() {
for (size_t i = 0; i < Props.size(); i++) {
if (!Props[i].has_value()) {
return static_cast<int>(i);
}
}
return -1;
}
prop* prop_id_lookup(int id)
{
if (id < 0 || id >= static_cast<int>(Props.size()) || !Props[id].has_value()) {
Assertion(false, "Could not find prop for id %d", id);
return nullptr;
}
return &Props[id].value();
}
int prop_category_id_lookup(const SCP_string& category)
{
for (size_t i = 0; i < Prop_categories.size(); i++) {
if (!stricmp(category.c_str(), Prop_categories[i].name.c_str())) {
return static_cast<int>(i);
}
}
return -1;
}
prop_category* prop_get_category(int index)
{
if (!SCP_vector_inbounds(Prop_categories, index)) {
Assertion(false, "Invalid category index %d", index);
return nullptr;
}
return &Prop_categories[index];
}
void parse_prop_table(const char* filename)
{
read_file_text(filename, CF_TYPE_TABLES);
reset_parse();
if (optional_string("#PROP CATEGORIES")) {
while (optional_string("$Name:")) {
prop_category pc;
stuff_string(pc.name, F_NAME);
required_string("+Color:");
int rgb[3];
stuff_int_list(rgb, 3, ParseLookupType::RAW_INTEGER_TYPE);
gr_init_color(&pc.list_color, rgb[0], rgb[1], rgb[2]);
// Replace existing category if name matches (case-insensitive)
auto existing =
std::find_if(Prop_categories.begin(), Prop_categories.end(), [&pc](const prop_category& existing_pc) {
return !stricmp(existing_pc.name.c_str(), pc.name.c_str());
});
if (existing != Prop_categories.end()) {
*existing = pc; // Replace
} else {
Prop_categories.push_back(pc); // Add new
}
}
}
required_string("#PROPS");
while (optional_string("$Name:")) {
char fname[NAME_LENGTH];
prop_info* pip = nullptr;
bool first_time = false;
bool create_if_not_found = true;
bool remove_prop = false;
stuff_string(fname, F_NAME, NAME_LENGTH);
if (optional_string("+nocreate")) {
if (!Parsing_modular_table) {
Warning(LOCATION, "+nocreate flag used for prop in non-modular table");
}
create_if_not_found = false;
}
// check first if this is on the remove blacklist
auto it = std::find(Removed_props.begin(), Removed_props.end(), fname);
if (it != Removed_props.end()) {
remove_prop = true;
}
// we might have a remove tag
if (optional_string("+remove")) {
if (!Parsing_modular_table) {
Warning(LOCATION, "+remove flag used for prop in non-modular table");
}
if (!remove_prop) {
Removed_props.emplace_back(fname);
remove_prop = true;
}
}
// Check if prop exists already
int prop_id = prop_info_lookup(fname);
// maybe remove it
if (remove_prop) {
if (prop_id >= 0) {
mprintf(("Removing previously parsed prop '%s'\n", fname));
Prop_info.erase(Prop_info.begin() + prop_id);
}
if (!skip_to_start_of_string_either("$Name:", "#End")) {
error_display(1, "Missing [#End] or [$Name] after prop class %s", fname);
}
continue;
}
// an entry for this prop exists
if (prop_id >= 0) {
pip = &Prop_info[prop_id];
if (!Parsing_modular_table) {
Warning(LOCATION,
"Error: Prop name %s already exists in %s. All prop class names must be unique.",
fname,
filename);
if (!skip_to_start_of_string_either("$Name:", "#End")) {
error_display(1, "Missing [#End] or [$Name] after duplicate prop class %s", fname);
}
continue;
}
}
// an entry does not exist
else {
// Don't create prop 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 prop class %s", fname);
}
continue;
}
Prop_info.emplace_back(prop_info());
pip = &Prop_info.back();
first_time = true;
pip->name = fname;
pip->category_index = -1;
}
if (optional_string("$POF file:")) {
char temp[32];
stuff_string(temp, F_NAME, MAX_FILENAME_LEN);
bool valid = true;
// if this is a modular table, and we're replacing an existing file name, and the file doesn't exist, don't replace it
if (Parsing_modular_table) {
if (VALID_FNAME(pip->pof_file)) {
if (!model_exists(temp)) {
valid = false;
}
}
}
if (valid) {
pip->pof_file = temp;
} else {
WarningEx(LOCATION, "Prop %s\nPOF file \"%s\" invalid!", pip->name.c_str(), temp);
}
}
if (optional_string("$Closeup_pos:")) {
stuff_vec3d(&pip->closeup_pos);
} else if (first_time && VALID_FNAME(pip->pof_file)) {
// Calculate from the model file. This is inefficient, but whatever
int model_idx = model_load(pip->pof_file.c_str());
polymodel* pm = model_get(model_idx);
// Go through, find best
pip->closeup_pos.xyz.z = fabsf(pm->maxs.xyz.z);
float temp = fabsf(pm->mins.xyz.z);
if (temp > pip->closeup_pos.xyz.z)
pip->closeup_pos.xyz.z = temp;
// Now multiply by 2
pip->closeup_pos.xyz.z *= -2.0f;
// We're done with the model.
model_unload(model_idx);
}
if (optional_string("$Closeup_zoom:")) {
stuff_float(&pip->closeup_zoom);
if (pip->closeup_zoom <= 0.0f) {
mprintf(("Warning! Prop '%s' has a $Closeup_zoom value that is less than or equal to 0 (%f). Setting "
"to default value.\n",
pip->name.c_str(),
pip->closeup_zoom));
pip->closeup_zoom = 0.5f;
}
}
if(optional_string("$Detail distance:")) {
pip->num_detail_levels = (int)stuff_int_list(pip->detail_distance, MAX_PROP_DETAIL_LEVELS, ParseLookupType::RAW_INTEGER_TYPE);
}
if (optional_string("$Category:")) {
SCP_string cat;
stuff_string(cat, F_NAME);
pip->category_index = prop_category_id_lookup(cat);
}
if (optional_string("$Flags:")) {
SCP_vector<SCP_string> prop_strings;
stuff_string_list(prop_strings);
for (const auto& flag : prop_strings) {
// get ship type from ship flags
const char* cur_flag = flag.c_str();
bool flag_found = false;
// check various ship flags
for (auto& pflag : Prop_flags) {
if (!stricmp(pflag.name, cur_flag)) {
flag_found = true;
if (!pflag.in_use) {
Warning(LOCATION,
"Use of '%s' flag for Prop Class '%s' - this flag is no longer needed.",
pflag.name,
pip->name.c_str());
} else {
pip->flags.set(pflag.def);
}
break;
}
}
// catch typos or deprecations
if (!stricmp(cur_flag, "no-collide") || !stricmp(cur_flag, "no_collide")) {
flag_found = true;
pip->flags.set(Prop::Info_Flags::No_collide);
}
if (!flag_found) {
Warning(LOCATION, "Bogus string in prop flags: %s\n", cur_flag);
}
}
}
if (optional_string("$Custom data:")) {
parse_string_map(pip->custom_data, "$end_custom_data", "+Val:");
}
if (optional_string("$Custom Strings")) {
while (optional_string("$Name:")) {
custom_string cs;
// The name of the string
stuff_string(cs.name, F_NAME);
// Arbitrary string value used for grouping strings together
required_string("+Value:");
stuff_string(cs.value, F_NAME);
// The string text itself
required_string("+String:");
stuff_string(cs.text, F_MULTITEXT);
pip->custom_strings.push_back(cs);
}
required_string("$end_custom_strings");
}
}
required_string("#END");
}
void post_process_props()
{
constexpr auto UnknownCategory = "Unknown Category";
bool create_unknown_category = false;
// check for missing data
for (auto& pi : Prop_info) {
if (!VALID_FNAME(pi.pof_file)) {
Warning(LOCATION, "Prop '%s' has no POF file specified!", pi.name.c_str());
}
if (!SCP_vector_inbounds(Prop_categories, pi.category_index)) {
Warning(LOCATION, "Prop '%s' has no or invalid category specified!", pi.name.c_str());
pi.category_index = -1; // Just to be safe, though it should already be -1 by default
create_unknown_category = true;
continue;
}
}
if (create_unknown_category) {
prop_category pc;
pc.name = UnknownCategory;
gr_init_color(&pc.list_color, 128, 128, 128);
Prop_categories.push_back(pc);
}
// Sort props by category order from Prop_categories, preserving internal order
std::stable_sort(Prop_info.begin(), Prop_info.end(), [](const prop_info& a, const prop_info& b) {
// Get index of a's category in Prop_categories
auto a_it = std::find_if(Prop_categories.begin(), Prop_categories.end(), [&](const prop_category& cat) {
return !stricmp(prop_get_category(a.category_index)->name.c_str(), cat.name.c_str());
});
int a_index = (a_it != Prop_categories.end()) ? static_cast<int>(std::distance(Prop_categories.begin(), a_it)) : INT_MAX;
// Get index of b's category in Prop_categories
auto b_it = std::find_if(Prop_categories.begin(), Prop_categories.end(), [&](const prop_category& cat) {
return !stricmp(prop_get_category(b.category_index)->name.c_str(), cat.name.c_str());
});
int b_index = (b_it != Prop_categories.end()) ? static_cast<int>(std::distance(Prop_categories.begin(), b_it)) : INT_MAX;
// Sort by category index
return a_index < b_index;
});
}
void prop_init()
{
// first parse the default table
if (cf_exists_full("props.tbl", CF_TYPE_TABLES)) {
parse_prop_table("props.tbl");
}
// parse any modular tables
parse_modular_table("*-prp.tbm", parse_prop_table);
post_process_props();
Props_inited = true;
}
/**
* Returns object index of prop.
* @return -1 means failed.
*/
int prop_create(const matrix* orient, const vec3d* pos, int prop_type, const char* name)
{
prop_info* pip;
prop* propp;
Assertion((prop_type >= 0) && (prop_type < static_cast<int>(Prop_info.size())),
"Invalid prop_type %d passed to prop_create() (expected value in the range 0-%d)\n",
prop_type,
static_cast<int>(Prop_info.size()) - 1);
pip = &(Prop_info[prop_type]);
int new_id = find_prop_empty_slot();
if (new_id < 0) {
Props.emplace_back(prop());
new_id = static_cast<int>(Props.size()) - 1;
} else {
Props[new_id] = prop();
}
propp = prop_id_lookup(new_id);
Assertion(propp != nullptr, "Could not create prop!");
propp->prop_info_index = prop_type;
if ((name == nullptr) || (prop_name_lookup(name) >= 0)) {
// regular name, regular suffix
char base_name[NAME_LENGTH];
char suffix[NAME_LENGTH];
strcpy_s(base_name, Prop_info[prop_type].name.c_str());
sprintf(suffix, NOX(" %d"), new_id);
// start building name
strcpy_s(propp->prop_name, base_name);
// if generated name will be longer than allowable name, truncate the class section of the name by the overflow
int char_overflow = static_cast<int>(strlen(base_name) + strlen(suffix)) - (NAME_LENGTH - 1);
if (char_overflow > 0) {
propp->prop_name[strlen(base_name) - static_cast<size_t>(char_overflow)] = '\0';
}
// complete building the name by adding suffix number
strcat_s(propp->prop_name, suffix);
} else {
strcpy_s(propp->prop_name, name);
}
if (!VALID_FNAME(pip->pof_file)) {
Error(LOCATION, "Cannot create prop %s; pof file is not valid", pip->name.c_str());
return -1;
}
pip->model_num = model_load(pip->pof_file.c_str());
polymodel* pm = model_get(pip->model_num);
if (pip->num_detail_levels != pm->n_detail_levels) {
if (!Is_standalone) {
// just log to file for standalone servers
Warning(LOCATION,
"For prop '%s', detail level\nmismatch. Table has %d,\nPOF has %d.",
pip->name.c_str(),
pip->num_detail_levels,
pm->n_detail_levels);
} else {
nprintf(("Warning",
"For prop '%s', detail level mismatch. Table has %d, POF has %d.",
pip->name.c_str(),
pip->num_detail_levels,
pm->n_detail_levels));
}
}
for (int i = 0; i < pm->n_detail_levels; i++)
pm->detail_depth[i] = (i < pip->num_detail_levels) ? i2fl(pip->detail_distance[i]) : 0.0f;
flagset<Object::Object_Flags> default_prop_object_flags;
default_prop_object_flags.set(Object::Object_Flags::Renders);
default_prop_object_flags.set(Object::Object_Flags::Physics);
default_prop_object_flags.set(Object::Object_Flags::Immobile);
default_prop_object_flags.set(Object::Object_Flags::Collides, !pip->flags[Prop::Info_Flags::No_collide]);
int objnum = obj_create(OBJ_PROP,
-1,
new_id,
orient,
pos,
model_get_radius(pip->model_num),
default_prop_object_flags);
Assert(objnum >= 0);
propp->objnum = objnum;
propp->model_instance_num = model_create_instance(objnum, pip->model_num);
object* objp = &Objects[objnum];
objp->hull_strength = 0.0f;
objp->phys_info.max_vel = vmd_zero_vector;
objp->phys_info.desired_vel = vmd_zero_vector;
objp->phys_info.forward_accel_time_const = 1.0;
objp->phys_info.flags = PF_CONST_VEL;
// For now use the asteroid signature for props as requested by Cyborg.
if (Game_mode & GM_MULTIPLAYER) {
objp->net_signature = multi_assign_network_signature(MULTI_SIG_ASTEROID);
}
// this is a little 'dangerous', values like this can cause the physics to do wierd things
// but they are the most 'honest', and will help to indicate when the physics is trying to use these values in ways they shouldn't
objp->phys_info.mass = INFINITY;
objp->phys_info.I_body_inv = vmd_zero_matrix;
// allocate memory for keeping glow point bank status (enabled/disabled)
{
bool val = true; // default value, enabled
if (pm->n_glow_point_banks)
propp->glow_point_bank_active.resize(pm->n_glow_point_banks, val);
// set any default off banks to off
for (int bank = 0; bank < pm->n_glow_point_banks; bank++) {
glow_point_bank_override* gpo = nullptr;
auto gpoi = pip->glowpoint_bank_override_map.find(bank);
if (gpoi != pip->glowpoint_bank_override_map.end()) {
gpo = (glow_point_bank_override*)pip->glowpoint_bank_override_map[bank];
}
if (gpo) {
if (gpo->default_off) {
propp->glow_point_bank_active[bank] = false;
}
}
}
}
//animation::anim_set_initial_states(propp);
// Start up stracking for this prop in multi.
//if (Game_mode & (GM_MULTIPLAYER)) {
//multi_rollback_ship_record_add_ship(objnum);
//}
// Set time when prop is created
propp->create_time = timer_get_milliseconds();
propp->time_created = Missiontime;
return objnum;
}
void prop_delete(object* obj)
{
int num = obj->instance;
int objnum = OBJ_INDEX(obj);
Assertion(Props[num].has_value(), "Props[%d] is already nullopt!", num);
prop& propp = *Props[num];
Assertion(propp.objnum == objnum, "Prop object id does not match passed object!");
propp.objnum = -1;
//animation::ModelAnimationSet::stopAnimations(model_get_instance(propp->model_instance_num));
// glow point banks
propp.glow_point_bank_active.clear();
model_delete_instance(propp.model_instance_num);
// Leave the slot empty for the duration of the scene
// The Props array will be compacted at the end of the level
Props[num] = std::nullopt;
}
/**
* Change the prop model for a prop to that for prop class 'prop_type'
*
* @param n index of prop in ::Props[] array
* @param prop_type prop class (index into ::Prop_info vector)
*/
static void prop_model_change(int n, int prop_type)
{
prop* sp = prop_id_lookup(n);
prop_info* sip = &(Prop_info[prop_type]);
object* objp = &Objects[sp->objnum];
//polymodel_instance* pmi = model_get_instance(sp->model_instance_num);
// get new model
if (sip->model_num == -1) {
sip->model_num = model_load(sip->pof_file.c_str());
}
polymodel* pm = model_get(sip->model_num);
Objects[sp->objnum].radius = model_get_radius(pm->id);
// page in nondims in game
if ( !Fred_running )
model_page_in_textures(sip->model_num, prop_type);
// allocate memory for keeping glow point bank status (enabled/disabled)
{
bool val = true; // default value, enabled
// clear out any old gpb's first, then add new ones if needed
sp->glow_point_bank_active.clear();
if (pm->n_glow_point_banks)
sp->glow_point_bank_active.resize( pm->n_glow_point_banks, val );
// set any default off banks to off
for (int bank = 0; bank < pm->n_glow_point_banks; bank++) {
glow_point_bank_override* gpo = nullptr;
auto gpoi = sip->glowpoint_bank_override_map.find(bank);
if (gpoi != sip->glowpoint_bank_override_map.end()) {
gpo = (glow_point_bank_override*)sip->glowpoint_bank_override_map[bank];
}
if (gpo) {
if (gpo->default_off) {
sp->glow_point_bank_active[bank] = false;
}
}
}
}
if ( sip->num_detail_levels != pm->n_detail_levels )
{
if ( !Is_standalone )
{
// just log to file for standalone servers
Warning(LOCATION, "For prop '%s', detail level\nmismatch. Table has %d,\nPOF has %d.", sip->name.c_str(), sip->num_detail_levels, pm->n_detail_levels );
}
else
{
nprintf(("Warning", "For prop '%s', detail level mismatch. Table has %d, POF has %d.", sip->name.c_str(), sip->num_detail_levels, pm->n_detail_levels ));
}
}
for (int i=0; i<pm->n_detail_levels; i++ )
pm->detail_depth[i] = (i < sip->num_detail_levels) ? i2fl(sip->detail_distance[i]) : 0.0f;
// reset texture animations
//sp->base_texture_anim_timestamp = _timestamp();
model_delete_instance(sp->model_instance_num);
// create new model instance data
// note: this is needed for both subsystem stuff and submodel animation stuff
sp->model_instance_num = model_create_instance(OBJ_INDEX(objp), sip->model_num);
//pmi = model_get_instance(sp->model_instance_num);
}
/**
* Change the prop class on a prop, and changing all required information
* for consistency
*
* @param n index of prop in ::Props[] array
* @param prop_type prop class (index into ::Prop_info vector)
* @param by_sexp SEXP reference
*/
void change_prop_type(int n, int prop_type)
{
prop* sp = prop_id_lookup(n);
// do a quick out if we're already using the new prop class
if (sp->prop_info_index == prop_type)
return;
int objnum = sp->objnum;
prop_info* pip = &(Prop_info[prop_type]);
prop_info* pip_orig = &Prop_info[sp->prop_info_index];
object* objp = &Objects[objnum];
// point to new prop data
prop_model_change(n, prop_type);
sp->prop_info_index = prop_type;
// check class-specific flags
if (pip->flags[Prop::Info_Flags::No_collide]) // changing TO a no-collision prop class
obj_set_flags(objp, objp->flags - Object::Object_Flags::Collides);
else if (pip_orig->flags[Prop::Info_Flags::No_collide]) // changing FROM a no-collision prop class
obj_set_flags(objp, objp->flags + Object::Object_Flags::Collides);
}
void prop_render(object* obj, model_draw_list* scene)
{
int num = obj->instance;
prop* propp = prop_id_lookup(num);
prop_info* pip = &Prop_info[propp->prop_info_index];
MONITOR_INC(NumPropsRend, 1);
model_render_params render_info;
auto pmi = model_get_instance(propp->model_instance_num);
auto pm = model_get(pmi->model_num);
model_clear_instance(pip->model_num);
model_instance_clear_arcs(pm, pmi);
uint64_t render_flags = MR_NORMAL;
if (propp->flags[Prop::Prop_Flags::Render_with_alpha_mult]) {
render_info.set_alpha_mult(propp->alpha_mult);
}
if (pip->flags[Prop::Info_Flags::No_lighting]) {
render_flags |= MR_NO_LIGHTING;
}
if (Rendering_to_shadow_map) {
render_flags = MR_NO_TEXTURING | MR_NO_LIGHTING;
}
if (propp->flags[Prop::Prop_Flags::Glowmaps_disabled]) {
render_flags |= MR_NO_GLOWMAPS;
}
if (propp->flags[Prop::Prop_Flags::Draw_as_wireframe]) {
render_flags |= MR_SHOW_OUTLINE_HTL | MR_NO_POLYS | MR_NO_TEXTURING;
render_info.set_color(Wireframe_color);
}
if (propp->flags[Prop::Prop_Flags::Render_full_detail]) {
render_flags |= MR_FULL_DETAIL;
}
if (propp->flags[Prop::Prop_Flags::Render_without_light]) {
render_flags |= MR_NO_LIGHTING;
}
uint debug_flags = render_info.get_debug_flags();
if (propp->flags[Prop::Prop_Flags::Render_without_diffuse]) {
debug_flags |= MR_DEBUG_NO_DIFFUSE;
}
if (propp->flags[Prop::Prop_Flags::Render_without_glowmap]) {
debug_flags |= MR_DEBUG_NO_GLOW;
}
if (propp->flags[Prop::Prop_Flags::Render_without_normalmap]) {
debug_flags |= MR_DEBUG_NO_NORMAL;
}
if (propp->flags[Prop::Prop_Flags::Render_without_ambientmap]) {
debug_flags |= MR_DEBUG_NO_AMBIENT;
}
if (propp->flags[Prop::Prop_Flags::Render_without_specmap]) {
debug_flags |= MR_DEBUG_NO_SPEC;
}
if (propp->flags[Prop::Prop_Flags::Render_without_reflectmap]) {
debug_flags |= MR_DEBUG_NO_REFLECT;
}
render_info.set_flags(render_flags);
render_info.set_debug_flags(debug_flags);
render_info.set_object_number(OBJ_INDEX(obj));
render_info.set_replacement_textures(pmi->texture_replace);
model_render_queue(&render_info, scene, pip->model_num, &obj->orient, &obj->pos);
}
// Draft. Props vector uses std::optional to allow for empty slots for deleted props so the indices of the remaining
// props do not change. In long FRED sessions without saving and loading, this can lead to a lot of empty slots. However,
// saving and loading the level will naturally compact the props vector by way of clearing and re-adding props.
// EDIT: Now that creating props will use empty prop slots, this may not be needed. Keeping it here for posterity.
/*void compact_props_vector()
{
SCP_vector<std::optional<prop>> new_props;
SCP_unordered_map<int, int> index_remap;
for (size_t i = 0; i < Props.size(); ++i) {
if (Props[i].has_value()) {
index_remap[static_cast<int>(i)] = static_cast<int>(new_props.size());
new_props.push_back(std::move(Props[i]));
}
}
Props = std::move(new_props);
// Remap object instances
for (object* obj = GET_FIRST(&obj_used_list); obj != END_OF_LIST(&obj_used_list); obj = GET_NEXT(obj)) {
if (obj->type == OBJ_PROP && obj->instance >= 0) {
auto it = index_remap.find(obj->instance);
Assertion(it != index_remap.end(), "Object is pointing to invalid prop index %d! Get a coder!", obj->instance);
if (it != index_remap.end()) {
obj->instance = it->second;
}
}
}
}*/
void props_level_init()
{
Props.clear();
}
void props_level_close()
{
// Clear all props and empty prop slots. Note that this can happen either before or after objects are cleaned up.
Props.clear();
}
// handles prop-ship/debris/asteroid/weapon collisions
int prop_check_collision(object* prop_obj, object* other_obj, vec3d* hitpos, collision_info_struct* prop_hit_info)
{
mc_info mc;
Assertion(prop_obj->type == OBJ_PROP, "Object is not a prop!");
int num = prop_obj->instance;
prop* propp = prop_id_lookup(num);
Assertion(propp->objnum == OBJ_INDEX(prop_obj), "Prop object id does not match passed object!");
prop_info* prinfo = &Prop_info[propp->prop_info_index];
// debris_hit_info nullptr - so debris-weapon collision
if (prop_hit_info == nullptr) {
// debris weapon collision
Assert(other_obj->type == OBJ_WEAPON);
mc.model_instance_num = propp->model_instance_num;
mc.model_num = prinfo->model_num; // Fill in the model to check
mc.orient = &prop_obj->orient; // The object's orient
mc.pos = &prop_obj->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;
}
weapon* wp = &Weapons[other_obj->instance];
wp->collisionInfo = new mc_info; // The weapon will free this memory later
*wp->collisionInfo = mc;
return mc.num_hits;
}
object* heavy_obj = prop_hit_info->heavy;
object* light_obj = prop_hit_info->light;
vec3d zero;
vm_vec_zero(&zero);
vec3d p0 = light_obj->last_pos - heavy_obj->last_pos;
vec3d p1 = light_obj->pos - heavy_obj->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 (heavy_obj->type == OBJ_SHIP && (vm_vec_mag_squared(&heavy_obj->phys_info.rotvel) * flFrametime * flFrametime) < (PI * PI / 36)) {
// collide_rotate calculate (1) start position and (2) relative velocity
prop_hit_info->collide_rotate = true;
vm_vec_rotate(&p0_temp, &p0, &heavy_obj->last_orient);
vm_vec_unrotate(&p0_rotated, &p0_temp, &heavy_obj->orient);
mc.p0 = &p0_rotated; // Point 1 of ray to check
vm_vec_sub(&prop_hit_info->light_rel_vel, &p1, &p0_rotated);
vm_vec_scale(&prop_hit_info->light_rel_vel, 1 / flFrametime);
}
else {
prop_hit_info->collide_rotate = false;
vm_vec_sub(&prop_hit_info->light_rel_vel, &light_obj->phys_info.vel, &heavy_obj->phys_info.vel);
}
int mc_ret_val = 0;
if (prop_hit_info->heavy == other_obj && other_obj->type == OBJ_SHIP) { // ship is heavier, so prop is sphere. Check sphere collision against ship poly model
mc.model_instance_num = Ships[other_obj->instance].model_instance_num;
mc.model_num = Ship_info[Ships[other_obj->instance].ship_info_index].model_num; // Fill in the model to check
mc.orient = &other_obj->orient; // The object's orient
mc.radius = model_get_core_radius(prinfo->model_num);
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
prop_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 (prop_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.
// turn off collision detection for all moving submodels
for (auto submodel : submodel_vector) {
pmi->submodel[submodel].collision_checked = true;
}
// Only check single submodel now, since children of moving submodels are handled as moving as well
mc.flags = orig_flags | MC_SUBMODEL;
if (Ship_info[Ships[other_obj->instance].ship_info_index].collision_lod > -1) {
mc.lod = Ship_info[Ships[other_obj->instance].ship_info_index].collision_lod;
}
// check each submodel in turn
for (auto submodel : submodel_vector) {
auto smi = &pmi->submodel[submodel];
// turn on just one submodel for collision test
smi->collision_checked = 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 < prop_hit_info->hit_time) {
mc_ret_val = 1;
// set up debris_hit_info common
set_hit_struct_info(prop_hit_info, &mc, true);
model_instance_local_to_global_point(&prop_hit_info->hit_pos, &mc.hit_point, pm, pmi, mc.hit_submodel, &heavy_obj->orient, &zero);
// set up debris_hit_info for rotating submodel
if (!prop_hit_info->edge_hit) {
model_instance_local_to_global_dir(&prop_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 = *mc.p1 - *mc.p0;
vec3d int_light_pos = *mc.p0 + diff * mc.hit_dist;
model_instance_local_to_global_point(&prop_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
smi->collision_checked = true;
}
}
// 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 < prop_hit_info->hit_time) {
mc_ret_val = 1;
set_hit_struct_info(prop_hit_info, &mc, false);
// get collision normal if not edge hit
if (!prop_hit_info->edge_hit) {
model_instance_local_to_global_dir(&prop_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 = *mc.p1 - *mc.p0;
prop_hit_info->light_collision_cm_pos = *mc.p0 + diff * mc.hit_dist;
}
}
}
} else { // any case OTHER than big ship small prop, simpler code
if (prop_obj == light_obj) {
mc.flags = (MC_CHECK_MODEL | MC_CHECK_SPHERELINE);
int instance = heavy_obj->instance;
// fill the appropriate model data
if (heavy_obj->type == OBJ_DEBRIS) {
mc.model_instance_num = -1;
mc.model_num = Debris[instance].model_num;
mc.submodel_num = Debris[instance].submodel_num;
mc.flags |= MC_SUBMODEL;
} else if (heavy_obj->type == OBJ_ASTEROID) {
asteroid* astp = &Asteroids[instance];
mc.model_instance_num = astp->model_instance_num;
mc.model_num = Asteroid_info[astp->asteroid_type].subtypes[astp->asteroid_subtype].model_number;
mc.submodel_num = -1;
}
mc.orient = &heavy_obj->orient; // The object's orient
mc.radius = model_get_core_radius(prinfo->model_num);
mc_ret_val = model_collide(&mc);
if (mc_ret_val) {
set_hit_struct_info(prop_hit_info, &mc, false);
// set normal if not edge hit
if (!prop_hit_info->edge_hit) {
vm_vec_unrotate(&prop_hit_info->collision_normal, &mc.hit_normal, &heavy_obj->orient);
}
// find position in submodel RF of light object at collison
vec3d diff = *mc.p1 - *mc.p0;
prop_hit_info->light_collision_cm_pos = *mc.p0 + diff * mc.hit_dist;
}
} else { // prop is the heavy object
mc.flags = (MC_CHECK_MODEL | MC_CHECK_SPHERELINE);
//int instance = heavy_obj->instance;
// fill the appropriate model data
mc.model_instance_num = propp->model_instance_num;
mc.model_num = prinfo->model_num;
mc.orient = &heavy_obj->orient;
switch (light_obj->type) {
case OBJ_SHIP:
mc.radius = model_get_core_radius(Ship_info[Ships[light_obj->instance].ship_info_index].model_num);
break;
case OBJ_ASTEROID: // FALLTHROUGH
case OBJ_DEBRIS:
mc.radius = light_obj->radius;
break;
default:
UNREACHABLE("Unknown object type in prop_check_collision");
};
mc_ret_val = model_collide(&mc);
if (mc_ret_val) {
set_hit_struct_info(prop_hit_info, &mc, false);
// set normal if not edge hit
if (!prop_hit_info->edge_hit) {
vm_vec_unrotate(&prop_hit_info->collision_normal, &mc.hit_normal, &heavy_obj->orient);
}
// find position in submodel RF of light object at collison
vec3d diff = *mc.p1 - *mc.p0;
prop_hit_info->light_collision_cm_pos = *mc.p0 + diff * mc.hit_dist;
}
}
}
if (mc_ret_val && other_obj->type == OBJ_SHIP) {
WarpEffect* warp_effect = nullptr;
ship* shipp = &Ships[other_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
prop_hit_info->r_heavy = prop_hit_info->hit_pos;
prop_hit_info->r_light = prop_hit_info->hit_pos - prop_hit_info->light_collision_cm_pos;
// set normal for edge hit
if (prop_hit_info->edge_hit) {
vm_vec_copy_normalize(&prop_hit_info->collision_normal, &prop_hit_info->r_light);
vm_vec_negate(&prop_hit_info->collision_normal);
}
// get world hitpos
vm_vec_add(hitpos, &prop_hit_info->heavy->pos, &prop_hit_info->r_heavy);
return 1;
}
else {
// no hit
return 0;
}
}