mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 22:56:30 -10:00
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]>
1129 lines
34 KiB
C++
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;
|
|
}
|
|
} |