Files
Goober5000andClaude Fable 5 7b241812ed make ship move-correct
Ship's implicit move operations compiled but were semantically broken in
two ways: moving copied the subsys_list sentinel's links verbatim,
leaving the bookend nodes of the intrusive subsystem list pointing at
the old sentinel address; and the raw owning warpin_effect /
warpout_effect pointers were copied without nulling the source, so a
vacated Ships[] slot aliased live WarpEffects that ship::clear() (on
slot reuse) or ship_close() (which walks every slot) would delete out
from under the destination ship.

Fix both at the member level so the compiler-generated moves are
correct by composition:

- Add ship_subsys_sentinel, derived from ship_subsys, whose move
  operations re-link the bookend nodes (handling the null-linked,
  self-referential-empty, and populated source states) and leave the
  moved-from sentinel as an initialized empty list.  Copying is
  deleted.  Public inheritance keeps &ship::subsys_list convertible to
  ship_subsys*, so list_init, GET_FIRST, and every other call site is
  unchanged.

- Convert warpin_effect and warpout_effect to std::unique_ptr.  The
  delete-and-null blocks in ship::clear(), ship_close(), and
  ship_set_warp_effects() collapse into reset(); call sites that borrow
  the pointer into a raw local now use get().

With the members handling themselves, declare ship's special member
functions: defaulted default constructor and moves, deleted copies
(previously implicit via the unique_ptr members, now stated intent).
The moves are deliberately not declared noexcept because members like
SCP_map and std::deque do not have standard-guaranteed nothrow moves;
the computed exception specification is used instead.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-20 23:04:56 -04:00

1129 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)
{
Assertion(id >= 0 && id < static_cast<int>(Props.size()) && Props[id].has_value(), "Could not find prop for id %d", id);
if (id < 0 || id >= static_cast<int>(Props.size()) || !Props[id].has_value()) {
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)
{
Assertion(SCP_vector_inbounds(Prop_categories, index), "Invalid category index %d", index);
if (!SCP_vector_inbounds(Prop_categories, 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(std::move(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(std::move(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(std::move(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 = &glowpoint_bank_overrides[gpoi->second];
}
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);
propp.model_instance_num = -1;
// 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
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 = &glowpoint_bank_overrides[gpoi->second];
}
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 (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.
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;
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) {
// 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 < 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
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 < 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 %d in prop_check_collision", light_obj->type);
};
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.get();
else if ((shipp->flags[Ship::Ship_Flags::Depart_warp]) && (shipp->warpout_effect != nullptr))
warp_effect = shipp->warpout_effect.get();
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;
}
}