From a1215c6d6fc57a393b2db67e6ae7654d9b0efedb Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 7 Mar 2021 21:30:21 -0500 Subject: [PATCH 1/7] infrastructure for handling submodel translations - maintaining, colliding, rendering --- code/ai/aicode.cpp | 4 +- code/model/model.h | 18 +++ code/model/model_flags.h | 1 + code/model/modelcollide.cpp | 24 ++-- code/model/modelread.cpp | 130 ++++++++++++++++------ code/model/modelrender.cpp | 9 +- code/network/multi.h | 3 +- code/network/multi_obj.cpp | 58 +++++++++- code/network/multi_obj.h | 4 +- code/scripting/api/objs/modelinstance.cpp | 23 ++++ code/scripting/api/objs/subsystem.cpp | 25 +++++ fred2/fredrender.cpp | 5 +- 12 files changed, 251 insertions(+), 53 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index b12d18ff63b..d3dc9404cf3 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -2792,7 +2792,7 @@ void copy_xlate_model_path_points(object *objp, model_path *mp, int dir, int cou auto pm = model_get(pmi->model_num); // Goober5000 - check for moving submodels - if ((mp->parent_submodel >= 0) && (pm->submodel[mp->parent_submodel].rotation_type >= 0)) + if ((mp->parent_submodel >= 0) && ((pm->submodel[mp->parent_submodel].rotation_type >= 0) || (pm->submodel[mp->parent_submodel].translation_type >= 0))) { moving_submodel = true; @@ -9444,7 +9444,7 @@ int find_parent_moving_submodel(polymodel *pm, int dock_index) submodel = pm->paths[path_num].parent_submodel; // submodel must exist and must move - if ((submodel >= 0) && (submodel < pm->n_models) && (pm->submodel[submodel].rotation_type >= 0)) + if ((submodel >= 0) && (submodel < pm->n_models) && ((pm->submodel[submodel].rotation_type >= 0) || (pm->submodel[submodel].translation_type >= 0))) { return submodel; } diff --git a/code/model/model.h b/code/model/model.h index 056a084b06c..6d731e5d8e0 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -90,6 +90,14 @@ struct submodel_instance float turn_accel = 0.0f; TIMESTAMP stepped_rotation_started; + float cur_offset = 0.0f; + float prev_offset = 0.0f; + + float current_shift_rate = 0.0f; + float desired_shift_rate = 0.0f; + float shift_accel = 0.0f; + int shift_step_zero_timestamp = timestamp(); // timestamp determines when next step is to begin (for stepped translation) + bool blown_off = false; // If set, this subobject is blown off bool collision_checked = false; @@ -97,6 +105,9 @@ struct submodel_instance // and should almost never be written directly. In most cases, coders should prefer cur_angle and prev_angle. matrix canonical_orient = vmd_identity_matrix; matrix canonical_prev_orient = vmd_identity_matrix; + // similarly for translation + vec3d canonical_offset = vmd_zero_vector; + vec3d canonical_prev_offset = vmd_zero_vector; // --- these fields used to be in bsp_info --- @@ -350,9 +361,16 @@ class bsp_info vec3d rotation_axis = vmd_zero_vector; // which axis this subobject rotates on. int rotation_axis_id = MOVEMENT_AXIS_NONE; // for optimization + int translation_type = MOVEMENT_TYPE_NONE; + vec3d translation_axis = vmd_zero_vector; + int translation_axis_id = MOVEMENT_AXIS_NONE; + float default_turn_rate = 0.0f; float default_turn_accel = 0.0f; + float default_shift_rate = 0.0f; + float default_shift_accel = 0.0f; + flagset flags; int subsys_num = -1; // subsystem number; index to ship_info->subsystems; the counterpart of model_subsystem->subobj_num diff --git a/code/model/model_flags.h b/code/model/model_flags.h index 8a33fcd0833..0da198dd488 100644 --- a/code/model/model_flags.h +++ b/code/model/model_flags.h @@ -25,6 +25,7 @@ namespace Model { FLAG_LIST(Subsystem_Flags) { Rotates, // This means the object rotates automatically + Translates, // This means the object translates automatically Stepped_rotate, // This means that the rotation occurs in steps Ai_rotate, // This means that the rotation is controlled by ai Crewpoint, // If set, this is a crew point. diff --git a/code/model/modelcollide.cpp b/code/model/modelcollide.cpp index 3aa1d4412b4..aa5b6700625 100644 --- a/code/model/modelcollide.cpp +++ b/code/model/modelcollide.cpp @@ -1060,25 +1060,25 @@ void mc_check_subobj( int mn ) // Check all of this subobject's children i = sm->first_child; while ( i >= 0 ) { - matrix instance_orient; - bool blown_off; - bool collision_checked; - bsp_info * csm = &Mc_pm->submodel[i]; + auto csm = &Mc_pm->submodel[i]; + matrix instance_orient = vmd_identity_matrix; + vec3d instance_offset = csm->offset; + bool blown_off = false; + bool collision_checked = false; if ( Mc_pmi ) { - instance_orient = Mc_pmi->submodel[i].canonical_orient; - blown_off = Mc_pmi->submodel[i].blown_off; - collision_checked = Mc_pmi->submodel[i].collision_checked; - } else { - instance_orient = vmd_identity_matrix; - blown_off = false; - collision_checked = false; + auto csmi = &Mc_pmi->submodel[i]; + instance_orient = csmi->canonical_orient; + vm_vec_add2(&instance_offset, &csmi->canonical_offset); + + blown_off = csmi->blown_off; + collision_checked = csmi->collision_checked; } // Don't check it or its children if it is destroyed // or if it's set to no collision if ( !blown_off && !collision_checked && !csm->flags[Model::Submodel_flags::No_collisions] ) { - vm_vec_unrotate(&Mc_base, &csm->offset, &saved_orient); + vm_vec_unrotate(&Mc_base, &instance_offset, &saved_orient); vm_vec_add2(&Mc_base, &saved_base); vm_matrix_x_matrix(&Mc_orient, &saved_orient, &instance_orient); diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index b23637773cc..0a2509e6a05 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -55,6 +55,7 @@ flag_def_list model_render_flags[] = int model_render_flags_size = sizeof(model_render_flags)/sizeof(flag_def_list); #define MAX_SUBMODEL_COLLISION_ANGULAR_VELOCITY (PI / 6.0f) // max 30 degrees per frame +#define MAX_SUBMODEL_COLLISION_LINEAR_VELOCITY 100.0f // max 100 meters per frame // info for special polygon lists @@ -83,21 +84,28 @@ static uint Global_checksum = 0; // compatible. #define PM_OBJFILE_MAJOR_VERSION 30 +// 23.01 adds support for submodel translation // 23.00 adds support for increased subobject vertex limit via TMAP2POLY // +// 22.02 adds support for submodel translation // 22.01 adds support for external weapon model angle offsets // 22.00 fixes the POF byte alignment and introduces the SLC2 chunk // +// 21.19 adds support for submodel translation // 21.18 adds support for external weapon model angle offsets // 21.17 adds support for engine thruster banks linked to specific engine subsystems // FreeSpace 2 shipped at POF version 21.17 // Descent: FreeSpace shipped at POF version 20.14 // See also https://wiki.hard-light.net/index.php/POF_data_structure -#define PM_LATEST_ALIGNED_VERSION 2300 -#define PM_LATEST_LEGACY_VERSION 2118 -#define PM_FIRST_ALIGNED_VERSION 2200 +#define PM_LATEST_VERTLIM_VERSION 2301 #define PM_FIRST_VERTLIM_VERSION 2300 +#define PM_LATEST_ALIGNED_VERSION 2202 +#define PM_FIRST_ALIGNED_VERSION 2200 + +#define PM_LATEST_LEGACY_VERSION 2119 + + static int Model_signature = 0; void interp_configure_vertex_buffers(polymodel*, int); @@ -620,7 +628,7 @@ bool in(char *&p, char *str, const char *substr) return p != nullptr; } -const Model::Subsystem_Flags carry_flags[] = { Model::Subsystem_Flags::Crewpoint, Model::Subsystem_Flags::Rotates, Model::Subsystem_Flags::Triggered, Model::Subsystem_Flags::Artillery, Model::Subsystem_Flags::Stepped_rotate }; +const Model::Subsystem_Flags carry_flags[] = { Model::Subsystem_Flags::Crewpoint, Model::Subsystem_Flags::Rotates, Model::Subsystem_Flags::Translates, Model::Subsystem_Flags::Triggered, Model::Subsystem_Flags::Artillery, Model::Subsystem_Flags::Stepped_rotate }; // Function to copy model data from one subsystem set to another subsystem set. This function // is called when two ships use the same model data, but since the model only gets read in one time, // the subsystem data is only present in one location. The ship code will call this routine to fix @@ -740,6 +748,7 @@ static void set_subsystem_info(int model_num, model_subsystem *subsystemp, char if (in(props, "$triggered")) { subsystemp->flags.set(Model::Subsystem_Flags::Rotates); + subsystemp->flags.set(Model::Subsystem_Flags::Translates); subsystemp->flags.set(Model::Subsystem_Flags::Triggered); } @@ -905,6 +914,12 @@ float get_submodel_delta_angle(const submodel_instance *smi) return delta_angle; } +float get_submodel_delta_shift(const submodel_instance *smi) +{ + // this is a bit simpler + return abs(smi->cur_offset - smi->prev_offset); +} + void do_new_subsystem( int n_subsystems, model_subsystem *slist, int subobj_num, float rad, vec3d *pnt, char *props, char *subobj_name, int model_num ) { int i; @@ -1183,10 +1198,9 @@ void extract_movement_info(bsp_info *sm, bool is_rotation, int *&movement_axis_i } else { - UNREACHABLE("Not yet implemented"); - movement_axis_id = nullptr; - movement_axis = nullptr; - movement_type = nullptr; + movement_axis_id = &sm->translation_axis_id; + movement_axis = &sm->translation_axis; + movement_type = &sm->translation_type; } } @@ -1209,7 +1223,7 @@ void determine_submodel_movement(bool is_rotation, const char *filename, bsp_inf *movement_axis = vmd_z_vector; else if (*movement_axis_id == MOVEMENT_AXIS_OTHER) { - auto axis_string = "$rotation_axis"; + auto axis_string = is_rotation ? "$rotation_axis" : "$translation_axis"; if (in(p, props, axis_string)) { @@ -1485,8 +1499,12 @@ int read_model_file(polymodel * pm, const char *filename, int n_subsystems, mode Warning(LOCATION,"Bad version (%d) in model file <%s>",version,filename); return 0; } - if ((version > PM_LATEST_LEGACY_VERSION && version < PM_FIRST_ALIGNED_VERSION) || (version > PM_LATEST_ALIGNED_VERSION)) { - Warning(LOCATION, "Model file %s is version %d, but the latest supported version on this build of FSO is %d. The model may not work correctly.", filename, version, version >= PM_FIRST_ALIGNED_VERSION ? PM_LATEST_ALIGNED_VERSION : PM_LATEST_LEGACY_VERSION); + if (version > PM_LATEST_LEGACY_VERSION && version < PM_FIRST_ALIGNED_VERSION) { + Warning(LOCATION, "Model file %s is version %d, but the latest supported version on this build of FSO is %d. The model may not work correctly.", filename, version, PM_LATEST_LEGACY_VERSION); + } else if (version > PM_LATEST_ALIGNED_VERSION && version < PM_FIRST_VERTLIM_VERSION) { + Warning(LOCATION, "Model file %s is version %d, but the latest supported version on this build of FSO is %d. The model may not work correctly.", filename, version, PM_LATEST_ALIGNED_VERSION); + } else if (version > PM_LATEST_VERTLIM_VERSION) { + Warning(LOCATION, "Model file %s is version %d, but the latest supported version on this build of FSO is %d. The model may not work correctly.", filename, version, PM_LATEST_VERTLIM_VERSION); } pm->version = version; @@ -1753,6 +1771,26 @@ int read_model_file(polymodel * pm, const char *filename, int n_subsystems, mode determine_submodel_movement(true, pm->filename, sm, props, look_at_submodel_names); + // submodel translation is a new POF feature + if ((pm->version >= 2119 && pm->version < PM_FIRST_ALIGNED_VERSION) + || (pm->version >= 2202 && pm->version < PM_FIRST_VERTLIM_VERSION) + || (pm->version >= 2301)) + { + sm->translation_type = cfread_int(fp); + sm->translation_axis_id = cfread_int(fp); + + if (sm->translation_type == MOVEMENT_TYPE_REGULAR) { + if (in(props, "$triggered")) { + sm->translation_type = MOVEMENT_TYPE_TRIGGERED; + } + } + + determine_submodel_movement(false, pm->filename, sm, props, look_at_submodel_names); + } else { + sm->translation_type = MOVEMENT_TYPE_NONE; + sm->translation_axis_id = -1; + } + if ( sm->name[0] == '\0' ) { strcpy_s(sm->name, "unknown object name"); } @@ -1763,9 +1801,10 @@ int read_model_file(polymodel * pm, const char *filename, int n_subsystems, mode get_user_prop_value(p+9, type); if ( !stricmp(type, "subsystem") ) { // if we have a subsystem, put it into the list! do_new_subsystem( n_subsystems, subsystems, n, sm->rad, &sm->offset, props, sm->name, pm->id ); - } else if ( !stricmp(type, "no_rotate") ) { - // mark those submodels which should not rotate - ie, those with no subsystem + } else if ( !stricmp(type, "no_rotate") || !stricmp(type, "no_translate") || !stricmp(type, "no_movement") ) { + // mark those submodels which should not move - i.e., those with no subsystem sm->rotation_type = MOVEMENT_TYPE_NONE; + sm->translation_type = MOVEMENT_TYPE_NONE; } else { // if submodel rotates (via bspgen), then there is either a subsys or special=no_rotate Assert( sm->rotation_type != MOVEMENT_TYPE_REGULAR ); @@ -1915,6 +1954,7 @@ int read_model_file(polymodel * pm, const char *filename, int n_subsystems, mode // ---------- submodel movement sanity checks ---------- do_movement_sanity_checks(true, sm, parent_sm, pm->filename); + do_movement_sanity_checks(false, sm, parent_sm, pm->filename); // ---------- done submodel movement sanity checks ---------- @@ -2768,7 +2808,7 @@ int read_model_file(polymodel * pm, const char *filename, int n_subsystems, mode // And now look through all the submodels and set the model flag if any are intrinsic-moving for (i = 0; i < pm->n_models; i++) { - if (pm->submodel[i].rotation_type == MOVEMENT_TYPE_INTRINSIC) { + if (pm->submodel[i].rotation_type == MOVEMENT_TYPE_INTRINSIC || pm->submodel[i].translation_type == MOVEMENT_TYPE_INTRINSIC) { pm->flags |= PM_FLAG_HAS_INTRINSIC_MOTION; break; } @@ -4144,6 +4184,7 @@ void model_local_to_global_point(vec3d *outpnt, const vec3d *mpnt, const polymod //instance up the tree for this point while ( (mn >= 0) && (pm->submodel[mn].parent >= 0) ) { // the angles in non-instanced models are always zero, so no need to rotate + // and no need to translate, for the same reason vm_vec_add2(&pnt, &pm->submodel[mn].offset); mn = pm->submodel[mn].parent; @@ -4178,7 +4219,8 @@ void model_instance_local_to_global_point(vec3d *outpnt, const vec3d *mpnt, cons //instance up the tree for this point while ( (mn >= 0) && (pm->submodel[mn].parent >= 0) ) { vm_vec_unrotate(&tpnt, &pnt, use_last_frame ? &pmi->submodel[mn].canonical_prev_orient : &pmi->submodel[mn].canonical_orient); - vm_vec_add(&pnt, &tpnt, &pm->submodel[mn].offset); + vm_vec_add(&pnt, &tpnt, use_last_frame ? &pmi->submodel[mn].canonical_prev_offset : &pmi->submodel[mn].canonical_offset); + vm_vec_add2(&pnt, &pm->submodel[mn].offset); mn = pm->submodel[mn].parent; } @@ -4205,7 +4247,8 @@ void model_instance_local_to_global_point_dir(vec3d *out_pnt, vec3d *out_dir, co // instance up the tree for this point while ( (mn >= 0) && (pm->submodel[mn].parent >= 0) ) { vm_vec_unrotate(&tpnt, &pnt, &pmi->submodel[mn].canonical_orient); - vm_vec_add(&pnt, &tpnt, &pm->submodel[mn].offset); + vm_vec_add(&pnt, &tpnt, &pmi->submodel[mn].canonical_offset); + vm_vec_add2(&pnt, &pm->submodel[mn].offset); vm_vec_unrotate(&tdir, &dir, &pmi->submodel[mn].canonical_orient); dir = tdir; @@ -4239,7 +4282,8 @@ void model_instance_local_to_global_point_orient(vec3d *outpnt, matrix *outorien // instance up the tree for this point while ( (mn >= 0) && (pm->submodel[mn].parent >= 0) ) { vm_vec_unrotate(&tpnt, &pnt, &pmi->submodel[mn].canonical_orient); - vm_vec_add(&pnt, &tpnt, &pm->submodel[mn].offset); + vm_vec_add(&pnt, &tpnt, &pmi->submodel[mn].canonical_offset); + vm_vec_add2(&pnt, &pm->submodel[mn].offset); orient = orient * pmi->submodel[mn].canonical_orient; @@ -4268,26 +4312,30 @@ void model_instance_global_to_local_point(vec3d* outpnt, const vec3d* mpnt, cons Assert(pm->id == pmi->model_num); constexpr int preallocatedStackDepth = 5; - std::pair preallocatedStack[preallocatedStackDepth]; + std::tuple preallocatedStack[preallocatedStackDepth]; - auto submodelStack = pm->submodel[submodel_num].depth <= preallocatedStackDepth ? preallocatedStack : new std::pair[pm->submodel[submodel_num].depth]; + auto submodelStack = pm->submodel[submodel_num].depth <= preallocatedStackDepth ? preallocatedStack : new std::tuple[pm->submodel[submodel_num].depth]; int stackCounter = 0; int mn = submodel_num; //Go up the chain of parents to build a stack of transformations from parent -> child while ((mn >= 0) && (pm->submodel[mn].parent >= 0)) { - if(use_last_frame) - submodelStack[stackCounter].first = &pmi->submodel[mn].canonical_prev_orient; - else - submodelStack[stackCounter].first = &pmi->submodel[mn].canonical_orient; - submodelStack[stackCounter++].second = &pm->submodel[mn].offset; + if(use_last_frame) { + std::get<0>(submodelStack[stackCounter]) = &pmi->submodel[mn].canonical_prev_orient; + std::get<1>(submodelStack[stackCounter]) = &pmi->submodel[mn].canonical_prev_offset; + } else { + std::get<0>(submodelStack[stackCounter]) = &pmi->submodel[mn].canonical_orient; + std::get<1>(submodelStack[stackCounter]) = &pmi->submodel[mn].canonical_offset; + } + std::get<2>(submodelStack[stackCounter++]) = &pm->submodel[mn].offset; mn = pm->submodel[mn].parent; } if (objorient != nullptr && objpos != nullptr) { - submodelStack[stackCounter].first = objorient; - submodelStack[stackCounter++].second = objpos; + std::get<0>(submodelStack[stackCounter]) = objorient; + std::get<1>(submodelStack[stackCounter]) = &vmd_zero_vector; + std::get<2>(submodelStack[stackCounter++]) = objpos; } stackCounter--; @@ -4296,8 +4344,9 @@ void model_instance_global_to_local_point(vec3d* outpnt, const vec3d* mpnt, cons while (stackCounter >= 0) { const auto& transform = submodelStack[stackCounter--]; - vm_vec_sub2(&resultPnt, transform.second); - vm_vec_rotate(&resultPnt, &resultPnt, transform.first); + vm_vec_sub2(&resultPnt, std::get<2>(transform)); + vm_vec_sub2(&resultPnt, std::get<1>(transform)); + vm_vec_rotate(&resultPnt, &resultPnt, std::get<0>(transform)); } *outpnt = resultPnt; @@ -4356,6 +4405,7 @@ void model_instance_global_to_local_dir(vec3d* out_dir, const vec3d* in_dir, con * 1) Have the rotating or intrinsic-rotating movement type * 2) Are currently rotating (i.e. actually moving and not part of the superstructure due to being destroyed or replaced) * 3) Are not rotating too far for collision detection (c.f. MAX_SUBMODEL_COLLISION_ANGULAR_VELOCITY) + * And check the translating equivalent as well */ void model_get_moving_submodel_list(SCP_vector &submodel_vector, const object *objp) { @@ -4405,8 +4455,10 @@ void model_get_moving_submodel_list(SCP_vector &submodel_vector, const obje if (child_submodel.rotation_type == MOVEMENT_TYPE_REGULAR || child_submodel.rotation_type == MOVEMENT_TYPE_INTRINSIC) { float delta_angle = get_submodel_delta_angle(&child_submodel_instance); isMoving |= delta_angle < MAX_SUBMODEL_COLLISION_ANGULAR_VELOCITY; - } - else if (child_submodel.flags[Model::Submodel_flags::Can_move]) { + } else if (child_submodel.translation_type == MOVEMENT_TYPE_REGULAR || child_submodel.translation_type == MOVEMENT_TYPE_INTRINSIC) { + float delta_shift = get_submodel_delta_shift(&child_submodel_instance); + isMoving |= delta_shift < MAX_SUBMODEL_COLLISION_LINEAR_VELOCITY; + } else if (child_submodel.flags[Model::Submodel_flags::Can_move]) { isMoving = true; } @@ -4503,6 +4555,10 @@ void model_set_submodel_instance_motion_info(bsp_info *sm, submodel_instance *sm smi->current_turn_rate = 0.0f; smi->desired_turn_rate = sm->default_turn_rate; smi->turn_accel = sm->default_turn_accel; + + smi->current_shift_rate = 0.0f; + smi->desired_shift_rate = sm->default_shift_rate; + smi->shift_accel = sm->default_shift_accel; } // Sets the submodel instance data when a tech room model instance is created. @@ -4555,10 +4611,18 @@ void model_replicate_submodel_instance_sub(polymodel *pm, polymodel_instance *pm r_smi->cur_angle = copy_from->cur_angle; r_smi->canonical_orient = copy_from->canonical_orient; r_smi->canonical_prev_orient = copy_from->canonical_prev_orient; + + r_smi->cur_offset = copy_from->cur_offset; + r_smi->canonical_offset = copy_from->canonical_offset; + r_smi->canonical_prev_offset = copy_from->canonical_prev_offset; } else { r_smi->cur_angle = smi->cur_angle; r_smi->canonical_orient = smi->canonical_orient; r_smi->canonical_prev_orient = smi->canonical_prev_orient; + + r_smi->cur_offset = smi->cur_offset; + r_smi->canonical_offset = smi->canonical_offset; + r_smi->canonical_prev_offset = smi->canonical_prev_offset; } } } else { @@ -4570,11 +4634,15 @@ void model_replicate_submodel_instance_sub(polymodel *pm, polymodel_instance *pm } } - // Set the angles. + // Set the angles and offset. if ( copy_from ) { smi->cur_angle = copy_from->cur_angle; smi->canonical_orient = copy_from->canonical_orient; smi->canonical_prev_orient = copy_from->canonical_prev_orient; + + smi->cur_offset = copy_from->cur_offset; + smi->canonical_offset = copy_from->canonical_offset; + smi->canonical_prev_offset = copy_from->canonical_prev_offset; } // For all the detail levels of this submodel, set them also. diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index 3e0868f24bb..ad4646130cc 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -1281,16 +1281,17 @@ void model_render_children_buffers(model_draw_list* scene, model_material *rende return; } - // Get submodel rotation data and use submodel orientation matrix - // to put together a matrix describing the final orientation of - // the submodel relative to its parent + // Get submodel rotation/translation data and use it to put together a matrix and a vector + // describing the final position of the submodel relative to its parent matrix submodel_orient = vmd_identity_matrix; + vec3d submodel_offset = sm->offset; if ( smi != nullptr ) { submodel_orient = smi->canonical_orient; + vm_vec_add2(&submodel_offset, &smi->canonical_offset); } - scene->push_transform(&sm->offset, &submodel_orient); + scene->push_transform(&submodel_offset, &submodel_orient); if ( (model_flags & MR_SHOW_OUTLINE || model_flags & MR_SHOW_OUTLINE_HTL || model_flags & MR_SHOW_OUTLINE_PRESET) && sm->outline_buffer != nullptr ) { diff --git a/code/network/multi.h b/code/network/multi.h index cd1775c49df..17d2ead72b2 100644 --- a/code/network/multi.h +++ b/code/network/multi.h @@ -71,9 +71,10 @@ class player; // version 55 - 8/28/2021 - Adding multi-compatible animations // version 56 - 8/28/2021 - Fix animations for 22_0 release // version 57 - 6/5/2022 - Upgrade interpolation, fix multiplayer sexp handling, and enable player orders to exceed 16 +// version 58 - ??/??/2022 - Submodel translation // STANDALONE_ONLY -#define MULTI_FS_SERVER_VERSION 57 +#define MULTI_FS_SERVER_VERSION 58 #define MULTI_FS_SERVER_COMPATIBLE_VERSION MULTI_FS_SERVER_VERSION diff --git a/code/network/multi_obj.cpp b/code/network/multi_obj.cpp index 8232b3579b8..5a88589a1dc 100644 --- a/code/network/multi_obj.cpp +++ b/code/network/multi_obj.cpp @@ -74,6 +74,9 @@ struct oo_info_sent_to_players { SCP_vector subsystem_2b; SCP_vector subsystem_2h; SCP_vector subsystem_2p; + SCP_vector subsystem_x; + SCP_vector subsystem_y; + SCP_vector subsystem_z; }; struct oo_netplayer_records{ @@ -354,6 +357,9 @@ void multi_rollback_ship_record_add_ship(int obj_num) Oo_info.player_frame_info[i].last_sent[net_sig_idx].subsystem_2b.push_back(-1.0f); Oo_info.player_frame_info[i].last_sent[net_sig_idx].subsystem_2h.push_back(-1.0f); Oo_info.player_frame_info[i].last_sent[net_sig_idx].subsystem_2p.push_back(-1.0f); + Oo_info.player_frame_info[i].last_sent[net_sig_idx].subsystem_x.push_back(0.0f); + Oo_info.player_frame_info[i].last_sent[net_sig_idx].subsystem_y.push_back(0.0f); + Oo_info.player_frame_info[i].last_sent[net_sig_idx].subsystem_z.push_back(0.0f); } } @@ -909,7 +915,9 @@ void multi_oo_respawn_reset_info(object* objp) player_record.last_sent[objp->net_signature].subsystem_2b[i] = -1.0f; player_record.last_sent[objp->net_signature].subsystem_2h[i] = -1.0f; player_record.last_sent[objp->net_signature].subsystem_2p[i] = -1.0f; - + player_record.last_sent[objp->net_signature].subsystem_x[i] = 0.0f; + player_record.last_sent[objp->net_signature].subsystem_y[i] = 0.0f; + player_record.last_sent[objp->net_signature].subsystem_z[i] = 0.0f; } } @@ -1373,6 +1381,27 @@ int multi_oo_pack_data(net_player *pl, object *objp, ushort oo_flags, ubyte *dat delete angs_1; delete angs_2; } + + // ditto for translation + if (subsystem->system_info->flags[Model::Subsystem_Flags::Translates]) { + auto smi = subsystem->submodel_instance_1; + + if (smi && smi->canonical_offset.xyz.x != Oo_info.player_frame_info[pl->player_id].last_sent[objp->net_signature].subsystem_x[i]) { + flags[i] |= OO_SUBSYS_TRANSLATION_x; + subsys_data.push_back(smi->canonical_offset.xyz.x); + } + + if (smi && smi->canonical_offset.xyz.y != Oo_info.player_frame_info[pl->player_id].last_sent[objp->net_signature].subsystem_y[i]) { + flags[i] |= OO_SUBSYS_TRANSLATION_y; + subsys_data.push_back(smi->canonical_offset.xyz.y); + } + + if (smi && smi->canonical_offset.xyz.z != Oo_info.player_frame_info[pl->player_id].last_sent[objp->net_signature].subsystem_z[i]) { + flags[i] |= OO_SUBSYS_TRANSLATION_z; + subsys_data.push_back(smi->canonical_offset.xyz.z); + } + } + i++; } @@ -1914,6 +1943,24 @@ int multi_oo_unpack_data(net_player* pl, ubyte* data, int seq_num, int time_delt data_idx++; } + if (flags[i] & OO_SUBSYS_TRANSLATION_x) { + subsysp->submodel_instance_1->canonical_prev_offset.xyz.x = subsysp->submodel_instance_1->canonical_offset.xyz.x; + subsysp->submodel_instance_1->canonical_offset.xyz.x = subsys_data[data_idx]; + data_idx++; + } + + if (flags[i] & OO_SUBSYS_TRANSLATION_y) { + subsysp->submodel_instance_1->canonical_prev_offset.xyz.y = subsysp->submodel_instance_1->canonical_offset.xyz.y; + subsysp->submodel_instance_1->canonical_offset.xyz.y = subsys_data[data_idx]; + data_idx++; + } + + if (flags[i] & OO_SUBSYS_TRANSLATION_z) { + subsysp->submodel_instance_1->canonical_prev_offset.xyz.z = subsysp->submodel_instance_1->canonical_offset.xyz.z; + subsysp->submodel_instance_1->canonical_offset.xyz.z = subsys_data[data_idx]; + data_idx++; + } + // fix up the matrixes if (flags[i] & OO_SUBSYS_ROTATION_1) { vm_angles_2_matrix(&subsysp->submodel_instance_1->canonical_prev_orient, prev_angs_1); @@ -2542,6 +2589,15 @@ void multi_init_oo_and_ship_tracker() temp_sent_to_player.subsystem_2p.reserve(MAX_MODEL_SUBSYSTEMS); temp_sent_to_player.subsystem_2p.push_back(0.0f); + temp_sent_to_player.subsystem_x.reserve(MAX_MODEL_SUBSYSTEMS); + temp_sent_to_player.subsystem_x.push_back(0.0f); + + temp_sent_to_player.subsystem_y.reserve(MAX_MODEL_SUBSYSTEMS); + temp_sent_to_player.subsystem_y.push_back(0.0f); + + temp_sent_to_player.subsystem_z.reserve(MAX_MODEL_SUBSYSTEMS); + temp_sent_to_player.subsystem_z.push_back(0.0f); + temp_netplayer_records.last_sent.push_back(temp_sent_to_player); Oo_info.frame_info.push_back(temp_position_records); diff --git a/code/network/multi_obj.h b/code/network/multi_obj.h index 6de1c5ccbff..4b7d48ded14 100644 --- a/code/network/multi_obj.h +++ b/code/network/multi_obj.h @@ -50,7 +50,9 @@ struct weapon; #define OO_SUBSYS_ROTATION_2b (1<<4) // Did this subsystem's barrel rotation angles change #define OO_SUBSYS_ROTATION_2h (1<<5) // Did this subsystem's barrel rotation angles change #define OO_SUBSYS_ROTATION_2p (1<<6) // Did this subsystem's barrel rotation angles change -#define OO_SUBSYS_TRANSLATION (1<<7) // Only for backwards compatibility of future builds. +#define OO_SUBSYS_TRANSLATION_x (1<<7) // Did this subsystem's base translation coordinates change +#define OO_SUBSYS_TRANSLATION_y (1<<8) // Did this subsystem's base translation coordinates change +#define OO_SUBSYS_TRANSLATION_z (1<<9) // Did this subsystem's base translation coordinates change // combo values #define OO_SUBSYS_ROTATION_1 (OO_SUBSYS_ROTATION_1b | OO_SUBSYS_ROTATION_1h | OO_SUBSYS_ROTATION_1p) diff --git a/code/scripting/api/objs/modelinstance.cpp b/code/scripting/api/objs/modelinstance.cpp index 6707c5026d3..fb53db01122 100644 --- a/code/scripting/api/objs/modelinstance.cpp +++ b/code/scripting/api/objs/modelinstance.cpp @@ -148,6 +148,29 @@ ADE_VIRTVAR(Orientation, l_SubmodelInstance, "orientation", "Gets or sets the su return ade_set_args(L, "o", l_Matrix.Set(matrix_h(&smi->canonical_orient))); } +ADE_VIRTVAR(Offset, l_SubmodelInstance, "vector", "Gets or sets the translated submodel instance offset", "vector", "Offset, or zero vector if handle is not valid") +{ + submodelinstance_h *smih; + vec3d *vec = nullptr; + if (!ade_get_args(L, "o|o", l_SubmodelInstance.GetPtr(&smih), l_Vector.GetPtr(&vec))) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + if (!smih->IsValid()) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + if (ADE_SETTING_VAR && vec != nullptr) + { + auto smi = smih->Get(); + + smi->canonical_prev_offset = smi->canonical_offset; + smi->canonical_offset = *vec; + + smi->cur_offset = vm_vec_mag(vec); + } + + return ade_set_args(L, "o", l_Vector.Set(smih->Get()->canonical_offset)); +} + ADE_FUNC(findWorldPoint, l_SubmodelInstance, "vector", "Calculates the world coordinates of a point in a submodel's frame of reference", "vector", "Point, or empty vector if handle is not valid") { submodelinstance_h *smih; diff --git a/code/scripting/api/objs/subsystem.cpp b/code/scripting/api/objs/subsystem.cpp index 09558f11149..1bcb9241639 100644 --- a/code/scripting/api/objs/subsystem.cpp +++ b/code/scripting/api/objs/subsystem.cpp @@ -156,6 +156,31 @@ ADE_VIRTVAR(GunOrientation, l_Subsystem, "orientation", "Orientation of turret g return ade_set_args(L, "o", l_Matrix.Set(matrix_h(&smi->canonical_orient))); } +ADE_VIRTVAR(Offset, l_Subsystem, "vector", "Translated offset of subobject or turret base", "vector", "Offset, or zero vector if handle is not valid") +{ + ship_subsys_h *sso; + vec3d *vec = nullptr; + if (!ade_get_args(L, "o|o", l_Subsystem.GetPtr(&sso), l_Vector.GetPtr(&vec))) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + if (!sso->isSubsystemValid()) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + auto smi = sso->ss->submodel_instance_1; + if (smi == nullptr) + return ade_set_error(L, "o", l_Vector.Set(vmd_zero_vector)); + + if (ADE_SETTING_VAR && vec != nullptr) + { + smi->canonical_prev_offset = smi->canonical_offset; + smi->canonical_offset = *vec; + + smi->cur_offset = vm_vec_mag(vec); + } + + return ade_set_args(L, "o", l_Vector.Set(smi->canonical_offset)); +} + ADE_VIRTVAR(HitpointsLeft, l_Subsystem, "number", "Subsystem hitpoints left", "number", "Hitpoints left, or 0 if handle is invalid. Setting a value of 0 will disable it - set a value of -1 or lower to actually blow it up.") { ship_subsys_h *sso; diff --git a/fred2/fredrender.cpp b/fred2/fredrender.cpp index e90f7178dff..de94f61a622 100644 --- a/fred2/fredrender.cpp +++ b/fred2/fredrender.cpp @@ -751,7 +751,10 @@ void fredhtl_render_subsystem_bounding_box(subsys_to_render *s2r) int mn = subobj_num; while ((mn >= 0) && (pm->submodel[mn].parent >= 0)) { - g3_start_instance_matrix(&pm->submodel[mn].offset, &pmi->submodel[mn].canonical_orient, true); + vec3d offset = pm->submodel[mn].offset; + vm_vec_add2(&offset, &pmi->submodel[mn].canonical_offset); + + g3_start_instance_matrix(&offset, &pmi->submodel[mn].canonical_orient, true); g3_count++; mn = pm->submodel[mn].parent; } From 5cf412aad84e1f40a1cea31812dbd5c58b219258 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 22 Jun 2022 23:25:26 -0400 Subject: [PATCH 2/7] implement stepped and regular translation as controlled by subsystems --- code/model/model.h | 23 ++- code/model/model_flags.h | 2 + code/model/modelread.cpp | 322 ++++++++++++++++++++++++++++++++++----- code/ship/ship.cpp | 29 ++++ code/ship/ship_flags.h | 1 + code/ship/shipfx.cpp | 3 +- 6 files changed, 340 insertions(+), 40 deletions(-) diff --git a/code/model/model.h b/code/model/model.h index 6d731e5d8e0..d2defddbdfa 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -96,7 +96,7 @@ struct submodel_instance float current_shift_rate = 0.0f; float desired_shift_rate = 0.0f; float shift_accel = 0.0f; - int shift_step_zero_timestamp = timestamp(); // timestamp determines when next step is to begin (for stepped translation) + TIMESTAMP stepped_translation_started; bool blown_off = false; // If set, this subobject is blown off bool collision_checked = false; @@ -150,6 +150,17 @@ typedef struct stepped_rotation { bool backwards; // if rate is negative } stepped_rotation_t; +typedef struct stepped_translation { + bool reverse_after_step; // for back-and-forth motion + float step_distance; // linear displacement of one step + float fraction; // fraction of time in step spent in accel + float t_transit; // time spent moving from one step to next + float t_pause; // time at rest between steps + float max_shift_rate; // max shift rate going between steps + float max_shift_accel; // max accel going between steps + bool backwards; // if rate is negative +} stepped_translation_t; + struct queued_animation; // definition for model subsystems. @@ -196,6 +207,7 @@ class model_subsystem { /* contains rotation rate info */ // movement specific info int weapon_rotation_pbank; // weapon-controlled rotation - Goober5000 std::shared_ptr stepped_rotation; // turn rotation struct + std::shared_ptr stepped_translation; // shift translation struct // AWACS specific information float awacs_intensity; // awacs intensity of this subsystem @@ -1019,7 +1031,7 @@ extern int model_rotate_gun(object *objp, polymodel *pm, polymodel_instance *pmi // Rotates the angle of a submodel. Use this so the right unlocked axis // gets stuffed. -extern void submodel_canonicalize(bsp_info *sm, submodel_instance *smi, bool clamp); +extern void submodel_canonicalize_rotation(bsp_info *sm, submodel_instance *smi, bool clamp); extern void submodel_rotate(model_subsystem *psub, submodel_instance *smi); extern void submodel_rotate(bsp_info *sm, submodel_instance *smi); @@ -1027,6 +1039,13 @@ extern void submodel_rotate(bsp_info *sm, submodel_instance *smi); // gets stuffed. Does this for stepped rotations void submodel_stepped_rotate(model_subsystem *psub, submodel_instance *smi); +// Similar to above +extern void submodel_canonicalize_translation(bsp_info *sm, submodel_instance *smi); +extern void submodel_translate(model_subsystem *psub, submodel_instance *smi); +extern void submodel_translate(bsp_info *sm, submodel_instance *smi); + +void submodel_stepped_translate(model_subsystem *psub, submodel_instance *smi); + // ------- submodel transformations ------- // Goober5000 diff --git a/code/model/model_flags.h b/code/model/model_flags.h index 0da198dd488..9eee87b27b2 100644 --- a/code/model/model_flags.h +++ b/code/model/model_flags.h @@ -13,6 +13,7 @@ namespace Model { Do_not_scale_detail_distances, // if set should not scale boxes or spheres based on 'model detail' settings Gun_rotation, // for animated weapon models Instant_rotate_accel, // rotating submodels instantly reach their desired velocity + Instant_translate_accel, // ditto for translating submodels No_collisions, // for $no_collisions property - kazan Nocollide_this_only, //SUSHI: Like no_collisions, but not recursive. For the "replacement" collision model scheme. Collide_invisible, //SUSHI: If set, this submodel should allow collisions for invisible textures. For the "replacement" collision model scheme. @@ -27,6 +28,7 @@ namespace Model { Rotates, // This means the object rotates automatically Translates, // This means the object translates automatically Stepped_rotate, // This means that the rotation occurs in steps + Stepped_translate, // Ditto for translation Ai_rotate, // This means that the rotation is controlled by ai Crewpoint, // If set, this is a crew point. Awacs, // If set, this subsystem has AWACS capability diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 0a2509e6a05..666dfc8f499 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -628,7 +628,7 @@ bool in(char *&p, char *str, const char *substr) return p != nullptr; } -const Model::Subsystem_Flags carry_flags[] = { Model::Subsystem_Flags::Crewpoint, Model::Subsystem_Flags::Rotates, Model::Subsystem_Flags::Translates, Model::Subsystem_Flags::Triggered, Model::Subsystem_Flags::Artillery, Model::Subsystem_Flags::Stepped_rotate }; +const Model::Subsystem_Flags carry_flags[] = { Model::Subsystem_Flags::Crewpoint, Model::Subsystem_Flags::Rotates, Model::Subsystem_Flags::Translates, Model::Subsystem_Flags::Triggered, Model::Subsystem_Flags::Artillery, Model::Subsystem_Flags::Stepped_rotate, Model::Subsystem_Flags::Stepped_translate }; // Function to copy model data from one subsystem set to another subsystem set. This function // is called when two ships use the same model data, but since the model only gets read in one time, // the subsystem data is only present in one location. The ship code will call this routine to fix @@ -898,6 +898,118 @@ static void set_subsystem_info(int model_num, model_subsystem *subsystemp, char submodelp->default_turn_accel = turn_accel; } } + // Translating subsystem + else if ((idx = prop_string(props, &p, "$translate_rate", "$translate")) >= 0) { + subsystemp->flags.set(Model::Subsystem_Flags::Translates); + + // get value for continuous or stepped translation + get_user_prop_value(p, buf); // note: p points to the value since we used prop_string + + // $translate means $translate_rate; there is no $translate_time + float shift_rate = static_cast(atof(buf)); + + // *** determine how the subsys translates *** + + // CASE OF STEPPED TRANSLATION + if (in(props, "$stepped")) { + + subsystemp->stepped_translation.reset(new stepped_translation); + subsystemp->flags.set(Model::Subsystem_Flags::Stepped_translate); + + // get whether to reverse after the step + // (always reverse unless the props say explicitly not to) + if (in(p, props, "$reverse_after_step")) { + get_user_prop_value(p+19, buf); + if (stricmp(buf, "false")) { + subsystemp->stepped_translation->reverse_after_step = true; + } + } else { + subsystemp->stepped_translation->reverse_after_step = true; + } + + // get step distance + if (in(p, props, "$step_distance")) { + get_user_prop_value(p+14, buf); + float step_dist = (float)atof(buf); + if (step_dist < 0.0f) { + Warning(LOCATION, "In model %s, subsystem %s, $step_distance must not be negative!", modelp->filename, submodelp->name); + step_dist = 25.0f; + } + subsystemp->stepped_translation->step_distance = step_dist; + } else { + subsystemp->stepped_translation->step_distance = 25.0f; + } + + // get pause time + if (in(p, props, "$t_paused")) { + get_user_prop_value(p+9, buf); + float t_pause = (float)atof(buf); + if (t_pause < 0.0f) { + Warning(LOCATION, "In model %s, subsystem %s, $t_paused must not be negative!", modelp->filename, submodelp->name); + t_pause = 2.0f; + } + subsystemp->stepped_translation->t_pause = t_pause; + } else { + subsystemp->stepped_translation->t_pause = 2.0f; + } + + // get transition time - time to make a complete movement + subsystemp->stepped_translation->t_transit = fl_abs(subsystemp->stepped_translation->step_distance / shift_rate); + + // get fraction of time spent in accel + if (in(p, props, "$fraction_accel")) { + get_user_prop_value(p+15, buf); + float fraction = (float)atof(buf); + if (fraction < 0.0f || fraction > 0.5f) { + Warning(LOCATION, "In model %s, subsystem %s, $fraction_accel must not be negative and must be less than or equal to 0.5!", modelp->filename, submodelp->name); + fraction = 0.3f; + } + subsystemp->stepped_translation->fraction = fraction; + } else { + subsystemp->stepped_translation->fraction = 0.3f; + } + + float step_distance = subsystemp->stepped_translation->step_distance; + float t_trans = subsystemp->stepped_translation->t_transit; + float fraction = subsystemp->stepped_translation->fraction; + + // reverse the direction if we start out with reverse velocity + if (shift_rate < 0.0f) { + subsystemp->stepped_translation->backwards = true; + } + + subsystemp->stepped_translation->max_shift_accel = fl_near_zero(fraction) ? 0.0f : step_distance / (fraction * (1.0f - fraction) * t_trans * t_trans); + subsystemp->stepped_translation->max_shift_rate = step_distance / ((1.0f - fraction) * t_trans); + } + + // CASE OF NORMAL CONTINUOUS TRANSLATION + else { + if (submodelp) { + submodelp->default_shift_rate = shift_rate; + } + } + + float shift_accel = 0.5f; + if (in(p, props, "$translate_accel")) { + get_user_prop_value(p + 16, buf); + + if (!stricmp(buf, "instant")) { + if (submodelp) { + submodelp->flags.set(Model::Submodel_flags::Instant_translate_accel); + } + shift_accel = 0.0f; + } else { + shift_accel = static_cast(atof(buf)); + if (shift_accel < 0.0f) { + Warning(LOCATION, "Model %s, submodel %s, $translate_accel %f cannot be negative!", modelp->filename, dname, shift_accel); + shift_accel *= -1; + } + } + } + if (submodelp) { + submodelp->default_shift_accel = shift_accel; + } + } } // used in collision code to check if submodel rotates too far @@ -1801,13 +1913,21 @@ int read_model_file(polymodel * pm, const char *filename, int n_subsystems, mode get_user_prop_value(p+9, type); if ( !stricmp(type, "subsystem") ) { // if we have a subsystem, put it into the list! do_new_subsystem( n_subsystems, subsystems, n, sm->rad, &sm->offset, props, sm->name, pm->id ); - } else if ( !stricmp(type, "no_rotate") || !stricmp(type, "no_translate") || !stricmp(type, "no_movement") ) { - // mark those submodels which should not move - i.e., those with no subsystem - sm->rotation_type = MOVEMENT_TYPE_NONE; - sm->translation_type = MOVEMENT_TYPE_NONE; } else { - // if submodel rotates (via bspgen), then there is either a subsys or special=no_rotate - Assert( sm->rotation_type != MOVEMENT_TYPE_REGULAR ); + if ( !stricmp(type, "no_rotate") || !stricmp(type, "no_movement") ) { + // mark those submodels which should not move - i.e., those with no subsystem + sm->rotation_type = MOVEMENT_TYPE_NONE; + } else { + // if submodel rotates (via bspgen), then there is either a subsys or special=no_rotate + Assert( sm->rotation_type != MOVEMENT_TYPE_REGULAR ); + } + if ( !stricmp(type, "no_translate") || !stricmp(type, "no_movement") ) { + // mark those submodels which should not move - i.e., those with no subsystem + sm->translation_type = MOVEMENT_TYPE_NONE; + } else { + // if submodel translates (via bspgen), then there is either a subsys or special=no_translate + Assert( sm->translation_type != MOVEMENT_TYPE_REGULAR ); + } } } @@ -3783,7 +3903,7 @@ void model_get_rotating_submodel_axis(vec3d *model_axis, vec3d *world_axis, cons } // Normalize the submodel angle and convert float angle to angles struct -void submodel_canonicalize(bsp_info *sm, submodel_instance *smi, bool clamp) +void submodel_canonicalize_rotation(bsp_info *sm, submodel_instance *smi, bool clamp) { smi->canonical_prev_orient = smi->canonical_orient; @@ -3831,6 +3951,32 @@ void submodel_canonicalize(bsp_info *sm, submodel_instance *smi, bool clamp) } } +// Convert float displacement to vector, but no normalization (clamping) is needed +void submodel_canonicalize_translation(bsp_info *sm, submodel_instance *smi) +{ + smi->canonical_prev_offset = smi->canonical_offset; + + // get the vector + switch (sm->rotation_axis_id) + { + case MOVEMENT_AXIS_X: + vm_vec_copy_scale(&smi->canonical_offset, &vmd_x_vector, smi->cur_offset); + break; + + case MOVEMENT_AXIS_Y: + vm_vec_copy_scale(&smi->canonical_offset, &vmd_y_vector, smi->cur_offset); + break; + + case MOVEMENT_AXIS_Z: + vm_vec_copy_scale(&smi->canonical_offset, &vmd_z_vector, smi->cur_offset); + break; + + default: + vm_vec_copy_scale(&smi->canonical_offset, &sm->translation_axis, smi->cur_offset); + break; + } +} + // Does stepped rotation of a submodel void submodel_stepped_rotate(model_subsystem *psub, submodel_instance *smi) { @@ -3899,7 +4045,87 @@ void submodel_stepped_rotate(model_subsystem *psub, submodel_instance *smi) smi->cur_angle *= -1.0f; } - submodel_canonicalize(sm, smi, true); + submodel_canonicalize_rotation(sm, smi, true); +} + +// Does stepped translation of a submodel +void submodel_stepped_translate(model_subsystem *psub, submodel_instance *smi) +{ + Assert(psub->flags[Model::Subsystem_Flags::Stepped_translate]); + + if ( psub->subobj_num < 0 ) return; + + polymodel *pm = model_get(psub->model_num); + bsp_info *sm = &pm->submodel[psub->subobj_num]; + + if ( sm->translation_type != MOVEMENT_TYPE_REGULAR ) return; + + if (!smi->stepped_translation_started.isValid()) + smi->stepped_translation_started = _timestamp(); + + float elapsed_time = timestamp_since(smi->stepped_translation_started) / static_cast(MILLISECONDS_PER_SECOND); + + // save last offset + smi->prev_offset = smi->cur_offset; + + // linear displacement of one step + float step_size = psub->stepped_translation->step_distance; + + // get time to complete one step, including pause + float step_time = psub->stepped_translation->t_transit + psub->stepped_translation->t_pause; + + // step_offset_time is TIME into current step + float step_offset_time = static_cast(fmod(elapsed_time, step_time)); + + // get step we are on (round down) + int cur_step = static_cast(elapsed_time / step_time); + + // set base displacement to 0 for now + smi->cur_offset = 0.0f; + + // determine which phase of translation we're in + float coast_start_time = psub->stepped_translation->fraction * psub->stepped_translation->t_transit; + float decel_start_time = psub->stepped_translation->t_transit * (1.0f - psub->stepped_translation->fraction); + float pause_start_time = psub->stepped_translation->t_transit; + + float start_coast_dist = 0.5f * psub->stepped_translation->max_shift_accel * coast_start_time * coast_start_time; + + if (step_offset_time < coast_start_time) { + // do accel + float accel_time = step_offset_time; + smi->cur_offset += 0.5f * psub->stepped_translation->max_shift_accel * accel_time * accel_time; + smi->current_shift_rate = psub->stepped_translation->max_shift_accel * accel_time; + } else if (step_offset_time < decel_start_time) { + // do coast + float coast_time = step_offset_time - coast_start_time; + smi->cur_offset += start_coast_dist + psub->stepped_translation->max_shift_rate * coast_time; + smi->current_shift_rate = psub->stepped_translation->max_shift_rate; + } else if (step_offset_time < pause_start_time) { + // do decel + float time_to_pause = psub->stepped_translation->t_transit - step_offset_time; + smi->cur_offset += (step_size - 0.5f * psub->stepped_translation->max_shift_accel * time_to_pause * time_to_pause); + smi->current_shift_rate = psub->stepped_translation->max_shift_rate * time_to_pause; + } else { + // do pause + smi->cur_offset += step_size; + smi->current_shift_rate = 0.0f; + } + + // set correct displacement depending on whether we are alternating or moving continuously + if (psub->stepped_translation->reverse_after_step) { + if (cur_step % 2 == 1) { + smi->cur_offset = step_size - smi->cur_offset; + } + } else { + smi->cur_offset += cur_step * step_size; + } + + // if we're going backwards, flip the whole thing + if (psub->stepped_translation->backwards) { + smi->cur_offset *= -1.0f; + } + + submodel_canonicalize_translation(sm, smi); } // Instantly rotate a submodel (around its axis of rotation) so that it is oriented toward its look_at_submodel. @@ -3942,7 +4168,7 @@ void submodel_look_at(polymodel *pm, polymodel_instance *pmi, int submodel_num) { sm->look_at_offset = -(smi->cur_angle); - // ensure the offset is in the proper range (see submodel_canonicalize) + // ensure the offset is in the proper range (see submodel_canonicalize_rotation) while (sm->look_at_offset > PI2) sm->look_at_offset -= PI2; while (sm->look_at_offset < 0.0f) @@ -3956,7 +4182,7 @@ void submodel_look_at(polymodel *pm, polymodel_instance *pmi, int submodel_num) smi->current_turn_rate = smi->desired_turn_rate = (smi->cur_angle - smi->prev_angle) / flFrametime; // and now set the other submodel fields - submodel_canonicalize(sm, smi, true); + submodel_canonicalize_rotation(sm, smi, true); } // Rotates the angle of a submodel, when the submodel has a subsystem (which is almost always the case) @@ -3974,45 +4200,68 @@ void submodel_rotate(model_subsystem *psub, submodel_instance *smi) submodel_rotate(sm, smi); } -// Rotates the angle of a submodel. If the submodel has a subsystem, the execution flow should first go through the other -// submodel_rotate function before this one. (This function is called directly in the case of dumb_rotation.) -void submodel_rotate(bsp_info *sm, submodel_instance *smi) +// Translates the offset of a submodel, when the submodel has a subsystem +void submodel_translate(model_subsystem *psub, submodel_instance *smi) { - // save last angles - smi->prev_angle = smi->cur_angle; + bsp_info * sm; + + if ( psub->subobj_num < 0 ) return; + + polymodel *pm = model_get(psub->model_num); + sm = &pm->submodel[psub->subobj_num]; + + if ( sm->translation_type != MOVEMENT_TYPE_REGULAR ) return; + + submodel_translate(sm, smi); +} + +// Helper function for both rotation and translation +void submodel_movement_calc(float &prev_value, float &cur_value, float ¤t_rate, float desired_rate, float accel, bool instant_accel) +{ + // save last value + prev_value = cur_value; float delta; - if (sm->flags[Model::Submodel_flags::Instant_rotate_accel]) { - delta = smi->desired_turn_rate * flFrametime; - smi->current_turn_rate = smi->desired_turn_rate; + if (instant_accel) { + delta = desired_rate * flFrametime; + current_rate = desired_rate; } else { - // probably send in a calculated desired turn rate - float diff = smi->desired_turn_rate - smi->current_turn_rate; + // probably send in a calculated desired rate + float diff = desired_rate - current_rate; - float final_turn_rate; + float final_rate; if (diff > 0) { - final_turn_rate = smi->current_turn_rate + smi->turn_accel * flFrametime; - if (final_turn_rate > smi->desired_turn_rate) { - final_turn_rate = smi->desired_turn_rate; + final_rate = current_rate + accel * flFrametime; + if (final_rate > desired_rate) { + final_rate = desired_rate; } } else if (diff < 0) { - final_turn_rate = smi->current_turn_rate - smi->turn_accel * flFrametime; - if (final_turn_rate < smi->desired_turn_rate) { - final_turn_rate = smi->desired_turn_rate; + final_rate = current_rate - accel * flFrametime; + if (final_rate < desired_rate) { + final_rate = desired_rate; } } else { - final_turn_rate = smi->desired_turn_rate; + final_rate = desired_rate; } - delta = (smi->current_turn_rate + final_turn_rate) * 0.5f * flFrametime; - smi->current_turn_rate = final_turn_rate; + delta = (current_rate + final_rate) * 0.5f * flFrametime; + current_rate = final_rate; } - // Apply rotation in the axis of movement - smi->cur_angle += delta; + // Apply movement + cur_value += delta; +} +void submodel_rotate(bsp_info *sm, submodel_instance *smi) +{ + submodel_movement_calc(smi->prev_angle, smi->cur_angle, smi->current_turn_rate, smi->desired_turn_rate, smi->turn_accel, sm->flags[Model::Submodel_flags::Instant_rotate_accel]); + submodel_canonicalize_rotation(sm, smi, true); +} - submodel_canonicalize(sm, smi, true); +void submodel_translate(bsp_info *sm, submodel_instance *smi) +{ + submodel_movement_calc(smi->prev_offset, smi->cur_offset, smi->current_shift_rate, smi->desired_shift_rate, smi->shift_accel, sm->flags[Model::Submodel_flags::Instant_translate_accel]); + submodel_canonicalize_translation(sm, smi); } // Tries to move joints so that the turret points to the point dst. @@ -4116,8 +4365,8 @@ int model_rotate_gun(object *objp, polymodel *pm, polymodel_instance *pmi, ship_ base_delta = vm_interp_angle(&base_smi->cur_angle, desired_base_angle, step_size, limited_base_rotation); gun_delta = vm_interp_angle(&gun_smi->cur_angle, desired_gun_angle, step_size); - submodel_canonicalize(base_sm, base_smi, true); - submodel_canonicalize(gun_sm, gun_smi, true); + submodel_canonicalize_rotation(base_sm, base_smi, true); + submodel_canonicalize_rotation(gun_sm, gun_smi, true); //------------ // Set fields for turret rotation sounds @@ -5587,6 +5836,7 @@ void model_subsystem::reset() weapon_rotation_pbank = 0; stepped_rotation.reset(); + stepped_translation.reset(); awacs_intensity = 0.0f; awacs_radius = 0.0f; diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 8a6a6f337a9..626613464f2 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -7073,6 +7073,8 @@ static int subsys_set(int objnum, int ignore_subsys_info) ship_system->flags.set(Ship::Subsystem_Flags::No_aggregate); if (model_system->flags[Model::Subsystem_Flags::Rotates]) ship_system->flags.set(Ship::Subsystem_Flags::Rotates); + if (model_system->flags[Model::Subsystem_Flags::Translates]) + ship_system->flags.set(Ship::Subsystem_Flags::Translates); if (model_system->flags[Model::Subsystem_Flags::Player_turret_sound]) ship_system->flags.set(Ship::Subsystem_Flags::Play_sound_for_player); if (model_system->flags[Model::Subsystem_Flags::No_disappear]) @@ -18336,6 +18338,32 @@ void ship_do_submodel_rotation(ship *shipp, model_subsystem *psub, ship_subsys * } } +// Goober5000 +void ship_do_submodel_translation(ship *shipp, model_subsystem *psub, ship_subsys *pss) +{ + Assert(shipp); + Assert(psub); + Assert(pss); + + // check if we actually can translate + if ( !(pss->flags[Ship::Subsystem_Flags::Translates]) ){ + return; + } + + if (psub->flags[Model::Subsystem_Flags::Triggered]) { + //Triggered translation is handled by animation stepping. + //The flag doesn't do anything at all anymore, except prevent other translation types + return; + } + + // if we got this far, we can translate - so choose which method to use + if (psub->flags[Model::Subsystem_Flags::Stepped_translate] ) { + submodel_stepped_translate(psub, pss->submodel_instance_1); + } else { + submodel_translate(psub, pss->submodel_instance_1 ); + } +} + void ship_move_subsystems(object *objp) { Assertion(objp->type == OBJ_SHIP, "ship_move_subsystems should only be called for ships! objp type = %d", objp->type); @@ -18351,6 +18379,7 @@ void ship_move_subsystems(object *objp) // do solar/radar/gas/activator rotation here ship_do_submodel_rotation(shipp, psub, pss); + ship_do_submodel_translation(shipp, psub, pss); } } diff --git a/code/ship/ship_flags.h b/code/ship/ship_flags.h index 4373f986584..466222344f9 100644 --- a/code/ship/ship_flags.h +++ b/code/ship/ship_flags.h @@ -26,6 +26,7 @@ namespace Ship { Vanished, // allows subsystem to be made to disappear without a trace (for swapping it for a true model for example. Missiles_ignore_if_dead, // forces homing missiles to target hull if subsystem is dead before missile hits it. Rotates, + Translates, Damage_as_hull, // Applies armor damage instead of subsystem damge. - FUBAR No_aggregate, // exclude this subsystem from the aggregate subsystem-info tracking - Goober5000 Play_sound_for_player, // If this subsystem is a turret on a player ship, play firing sounds - The E diff --git a/code/ship/shipfx.cpp b/code/ship/shipfx.cpp index ea9fee865ec..58ed3d4e739 100644 --- a/code/ship/shipfx.cpp +++ b/code/ship/shipfx.cpp @@ -114,8 +114,7 @@ static void shipfx_subsystem_maybe_create_live_debris(object *ship_objp, ship *s model_get_rotating_submodel_axis(&model_axis, &world_axis, pm, pmi, submodel_num, &ship_objp->orient); vm_vec_copy_scale(&rotvel, &world_axis, smi->current_turn_rate); - //TODO replace zero_vector with translation offset of submodel - model_instance_local_to_global_point(&world_axis_pt, &vmd_zero_vector, pm, pmi, submodel_num, &ship_objp->orient, &ship_objp->pos); + model_instance_local_to_global_point(&world_axis_pt, &smi->canonical_offset, pm, pmi, submodel_num, &ship_objp->orient, &ship_objp->pos); vm_quaternion_rotate(&m_rot, smi->cur_angle, &model_axis); } else { From d3807cbd08e5f5dde4024903ca8375e02dd547cb Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 24 Jun 2022 01:59:26 -0400 Subject: [PATCH 3/7] add subsystem sexps --- code/parse/sexp.cpp | 169 +++++++++++++++++++++++++++++++++++++------- code/parse/sexp.h | 6 ++ fred2/sexp_tree.cpp | 22 +++++- 3 files changed, 169 insertions(+), 28 deletions(-) diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 0b012110afd..0f1e4302535 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -482,6 +482,10 @@ SCP_vector Operators = { { "free-rotating-subsystem", OP_FREE_ROTATING_SUBSYSTEM, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // Goober5000 { "reverse-rotating-subsystem", OP_REVERSE_ROTATING_SUBSYSTEM, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // Goober5000 { "rotating-subsys-set-turn-time", OP_ROTATING_SUBSYS_SET_TURN_TIME, 3, INT_MAX, SEXP_ACTION_OPERATOR, }, // Goober5000 + { "lock-translating-subsystem", OP_LOCK_TRANSLATING_SUBSYSTEM, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // Goober5000 + { "free-translating-subsystem", OP_FREE_TRANSLATING_SUBSYSTEM, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // Goober5000 + { "reverse-translating-subsystem", OP_REVERSE_TRANSLATING_SUBSYSTEM, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // Goober5000 + { "translating-subsys-set-speed", OP_TRANSLATING_SUBSYS_SET_SPEED, 3, INT_MAX, SEXP_ACTION_OPERATOR, }, // Goober5000 { "trigger-submodel-animation", OP_TRIGGER_SUBMODEL_ANIMATION, 4, 6, SEXP_ACTION_OPERATOR, }, // Goober5000 { "change-subsystem-name", OP_CHANGE_SUBSYSTEM_NAME, 3, INT_MAX, SEXP_ACTION_OPERATOR, }, // Karajorma { "ship-subsys-targetable", OP_SHIP_SUBSYS_TARGETABLE, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // Goober5000 @@ -2410,6 +2414,7 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, i case OPF_AWACS_SUBSYSTEM: case OPF_ROTATING_SUBSYSTEM: + case OPF_TRANSLATING_SUBSYSTEM: case OPF_SUBSYSTEM: case OPF_SUBSYSTEM_OR_NONE: case OPF_SUBSYS_OR_GENERIC: @@ -2556,6 +2561,12 @@ int check_sexp_syntax(int node, int return_type, int recursive, int *bad_node, i { return SEXP_CHECK_INVALID_ROTATING_SUBSYS; } + + // translating subsystem, like above - Goober5000 + if ((type == OPF_TRANSLATING_SUBSYSTEM) && !(Ship_info[ship_class].subsystems[i].flags[Model::Subsystem_Flags::Translates])) + { + return SEXP_CHECK_INVALID_TRANSLATING_SUBSYS; + } } break; @@ -20294,7 +20305,7 @@ int sexp_is_in_turret_fov(int node) } // Goober5000 -void sexp_set_subsys_rotation_lock_free(int node, bool locked) +void sexp_set_subsys_motion_lock_free(int node, bool is_rotation, bool locked) { // get the ship auto ship_entry = eval_ship(node); @@ -20310,37 +20321,44 @@ void sexp_set_subsys_rotation_lock_free(int node, bool locked) if (subsys == nullptr) continue; - // see if it's already at the state we want - if (subsys->flags[Ship::Subsystem_Flags::Rotates] == !locked) - continue; - - // set the flag - subsys->flags.set(Ship::Subsystem_Flags::Rotates, !locked); + // skip if it's already at the state we want; if not, set the flag + if (is_rotation) + { + if (subsys->flags[Ship::Subsystem_Flags::Rotates] == !locked) + continue; + subsys->flags.set(Ship::Subsystem_Flags::Rotates, !locked); + } + else + { + if (subsys->flags[Ship::Subsystem_Flags::Translates] == !locked) + continue; + subsys->flags.set(Ship::Subsystem_Flags::Translates, !locked); + } // set moving or not, depending on flag if (locked) { - if (subsys->subsys_snd_flags[Ship::Subsys_Sound_Flags::Rotate]) + if (is_rotation && subsys->subsys_snd_flags[Ship::Subsys_Sound_Flags::Rotate]) { obj_snd_delete_type(ship_entry->shipp->objnum, subsys->system_info->rotation_snd, subsys); subsys->subsys_snd_flags.remove(Ship::Subsys_Sound_Flags::Rotate); } // bit of a hack... set the timestamp to just the delta so that we can restore it properly later - auto &stamp = subsys->submodel_instance_1->stepped_rotation_started; + auto &stamp = is_rotation ? subsys->submodel_instance_1->stepped_rotation_started : subsys->submodel_instance_1->stepped_translation_started; if (stamp.isValid()) stamp = TIMESTAMP(timestamp_since(stamp)); } else { - if (subsys->system_info->rotation_snd.isValid()) + if (is_rotation && subsys->system_info->rotation_snd.isValid()) { obj_snd_assign(ship_entry->shipp->objnum, subsys->system_info->rotation_snd, &subsys->system_info->pnt, OS_SUBSYS_ROTATION, subsys); subsys->subsys_snd_flags.set(Ship::Subsys_Sound_Flags::Rotate); } // and restore the timestamp from the delta - auto &stamp = subsys->submodel_instance_1->stepped_rotation_started; + auto &stamp = is_rotation ? subsys->submodel_instance_1->stepped_rotation_started : subsys->submodel_instance_1->stepped_translation_started; if (stamp.isValid()) stamp = timestamp_delta(_timestamp(), -stamp.value()); } @@ -20348,7 +20366,7 @@ void sexp_set_subsys_rotation_lock_free(int node, bool locked) } // Goober5000 -void sexp_reverse_rotating_subsystem(int node) +void sexp_reverse_moving_subsystem(int node, bool is_rotation) { // get the ship auto ship_entry = eval_ship(node); @@ -20364,18 +20382,26 @@ void sexp_reverse_rotating_subsystem(int node) if (subsys == nullptr || subsys->submodel_instance_1 == nullptr) continue; - // switch direction of rotation - subsys->submodel_instance_1->current_turn_rate *= -1.0f; - subsys->submodel_instance_1->desired_turn_rate *= -1.0f; + // switch direction of motion + if (is_rotation) + { + subsys->submodel_instance_1->current_turn_rate *= -1.0f; + subsys->submodel_instance_1->desired_turn_rate *= -1.0f; + } + else + { + subsys->submodel_instance_1->current_shift_rate *= -1.0f; + subsys->submodel_instance_1->desired_shift_rate *= -1.0f; + } } } // Goober5000 -void sexp_rotating_subsys_set_turn_time(int node) +void sexp_moving_subsys_set_turn_time_or_speed(int node, bool is_rotation) { int n = node; bool is_nan, is_nan_forever; - float turn_time, turn_accel; + float time_or_speed, accel; // get the ship auto ship_entry = eval_ship(n); @@ -20389,23 +20415,44 @@ void sexp_rotating_subsys_set_turn_time(int node) return; n = CDR(n); - // get and set the turn time - turn_time = eval_num(n, is_nan, is_nan_forever) / 1000.0f; + // get and set the value + time_or_speed = eval_num(n, is_nan, is_nan_forever) / 1000.0f; if (is_nan || is_nan_forever) return; - subsys->submodel_instance_1->desired_turn_rate = PI2 / turn_time; + if (fl_near_zero(time_or_speed)) + { + Warning(LOCATION, "In %s, %s cannot be zero!", is_rotation ? "rotating-subsys-set-turn-time" : "translating-subsys-set-speed", is_rotation ? "turn time" : "speed"); + return; + } + if (is_rotation) + subsys->submodel_instance_1->desired_turn_rate = PI2 / time_or_speed; + else + subsys->submodel_instance_1->desired_shift_rate = time_or_speed; n = CDR(n); - // maybe get and set the turn accel + // maybe get and set the accel if (n != -1) { - turn_accel = eval_num(n, is_nan, is_nan_forever) / 1000.0f; + accel = eval_num(n, is_nan, is_nan_forever) / 1000.0f; if (is_nan || is_nan_forever) return; - subsys->submodel_instance_1->turn_accel = PI2 / turn_accel; + if (fl_near_zero(accel)) + { + Warning(LOCATION, "In %s, acceleration cannot be zero!", is_rotation ? "rotating-subsys-set-turn-time" : "translating-subsys-set-speed"); + return; + } + if (is_rotation) + subsys->submodel_instance_1->turn_accel = PI2 / accel; + else + subsys->submodel_instance_1->shift_accel = accel; } else - subsys->submodel_instance_1->current_turn_rate = PI2 / turn_time; + { + if (is_rotation) + subsys->submodel_instance_1->current_turn_rate = PI2 / time_or_speed; + else + subsys->submodel_instance_1->current_shift_rate = time_or_speed; + } } void sexp_trigger_submodel_animation(int node) @@ -26726,17 +26773,21 @@ int eval_sexp(int cur_node, int referenced_node) case OP_LOCK_ROTATING_SUBSYSTEM: case OP_FREE_ROTATING_SUBSYSTEM: - sexp_set_subsys_rotation_lock_free(node, op_num == OP_LOCK_ROTATING_SUBSYSTEM); + case OP_LOCK_TRANSLATING_SUBSYSTEM: + case OP_FREE_TRANSLATING_SUBSYSTEM: + sexp_set_subsys_motion_lock_free(node, op_num == OP_LOCK_ROTATING_SUBSYSTEM || op_num == OP_FREE_ROTATING_SUBSYSTEM, op_num == OP_LOCK_ROTATING_SUBSYSTEM || op_num == OP_LOCK_TRANSLATING_SUBSYSTEM); sexp_val = SEXP_TRUE; break; case OP_REVERSE_ROTATING_SUBSYSTEM: - sexp_reverse_rotating_subsystem(node); + case OP_REVERSE_TRANSLATING_SUBSYSTEM: + sexp_reverse_moving_subsystem(node, op_num == OP_REVERSE_ROTATING_SUBSYSTEM); sexp_val = SEXP_TRUE; break; case OP_ROTATING_SUBSYS_SET_TURN_TIME: - sexp_rotating_subsys_set_turn_time(node); + case OP_TRANSLATING_SUBSYS_SET_SPEED: + sexp_moving_subsys_set_turn_time_or_speed(node, op_num == OP_ROTATING_SUBSYS_SET_TURN_TIME); sexp_val = SEXP_TRUE; break; @@ -28153,6 +28204,10 @@ int query_operator_return_type(int op) case OP_FREE_ROTATING_SUBSYSTEM: case OP_REVERSE_ROTATING_SUBSYSTEM: case OP_ROTATING_SUBSYS_SET_TURN_TIME: + case OP_LOCK_TRANSLATING_SUBSYSTEM: + case OP_FREE_TRANSLATING_SUBSYSTEM: + case OP_REVERSE_TRANSLATING_SUBSYSTEM: + case OP_TRANSLATING_SUBSYS_SET_SPEED: case OP_TRIGGER_SUBMODEL_ANIMATION: case OP_PLAYER_USE_AI: case OP_PLAYER_NOT_USE_AI: @@ -30126,6 +30181,14 @@ int query_operator_argument_type(int op, int argnum) else return OPF_ROTATING_SUBSYSTEM; + case OP_LOCK_TRANSLATING_SUBSYSTEM: + case OP_FREE_TRANSLATING_SUBSYSTEM: + case OP_REVERSE_TRANSLATING_SUBSYSTEM: + if (argnum == 0) + return OPF_SHIP; + else + return OPF_TRANSLATING_SUBSYSTEM; + case OP_ROTATING_SUBSYS_SET_TURN_TIME: if (argnum == 0) return OPF_SHIP; @@ -30136,6 +30199,16 @@ int query_operator_argument_type(int op, int argnum) else return OPF_POSITIVE; + case OP_TRANSLATING_SUBSYS_SET_SPEED: + if (argnum == 0) + return OPF_SHIP; + else if (argnum == 1) + return OPF_TRANSLATING_SUBSYSTEM; + else if (argnum == 2) + return OPF_NUMBER; + else + return OPF_POSITIVE; + case OP_TRIGGER_SUBMODEL_ANIMATION: if (argnum == 0) return OPF_SHIP; @@ -31256,6 +31329,9 @@ const char *sexp_error_message(int num) case SEXP_CHECK_INVALID_ROTATING_SUBSYS: return "This must be a rotating subsystem"; + case SEXP_CHECK_INVALID_TRANSLATING_SUBSYS: + return "This must be a translating subsystem"; + case SEXP_CHECK_INVALID_SUBSYS_TYPE: return "Invalid subsystem type"; @@ -32515,6 +32591,10 @@ int get_subcategory(int sexp_id) case OP_FREE_ROTATING_SUBSYSTEM: case OP_REVERSE_ROTATING_SUBSYSTEM: case OP_ROTATING_SUBSYS_SET_TURN_TIME: + case OP_LOCK_TRANSLATING_SUBSYSTEM: + case OP_FREE_TRANSLATING_SUBSYSTEM: + case OP_REVERSE_TRANSLATING_SUBSYSTEM: + case OP_TRANSLATING_SUBSYS_SET_SPEED: case OP_TRIGGER_SUBMODEL_ANIMATION: case OP_CHANGE_SUBSYSTEM_NAME: case OP_SHIP_SUBSYS_TARGETABLE: @@ -36336,6 +36416,41 @@ SCP_vector Sexp_help = { "Omit this argument if you want an instantaneous change." }, + // Goober5000 + { OP_LOCK_TRANSLATING_SUBSYSTEM, "lock-translating-subsystem\r\n" + "\tInstantaneously locks a translating subsystem so that it cannot translate unless freed by free-translating-subsystem. " + "Takes 2 or more arguments...\r\n" + "\t1:\tName of the ship housing the subsystem\r\n" + "\tRest:\tName of the translating subsystem to lock" + }, + + // Goober5000 + { OP_FREE_TRANSLATING_SUBSYSTEM, "free-translating-subsystem\r\n" + "\tInstantaneously frees a translating subsystem previously locked by lock-translating-subsystem. " + "Takes 2 or more arguments...\r\n" + "\t1:\tName of the ship housing the subsystem\r\n" + "\tRest:\tName of the translating subsystem to free" + }, + + // Goober5000 + { OP_REVERSE_TRANSLATING_SUBSYSTEM, "reverse-translating-subsystem\r\n" + "\tInstantaneously reverses the translation direction of a translating subsystem. " + "Takes 2 or more arguments...\r\n" + "\t1:\tName of the ship housing the subsystem\r\n" + "\tRest:\tName of the translating subsystem to reverse" + }, + + // Goober5000 + { OP_TRANSLATING_SUBSYS_SET_SPEED, "translating-subsys-set-speed\r\n" + "\tSets the speed (movement rate) of a translating subsystem. " + "Takes 3 or 4 arguments...\r\n" + "\t1:\tName of the ship housing the subsystem\r\n" + "\t2:\tName of the translating subsystem to configure\r\n" + "\t3:\tThe movement rate, in millimeters per second (i.e. one thousand times the speed in m/s) - positive is along the vector of the translation axis\r\n" + "\t4:\tThe acceleration (x1000, just as #3 is m/s x1000) to change from the current movement rate to the desired movement rate. " + "Omit this argument if you want an instantaneous change." + }, + // Goober5000 { OP_TRIGGER_SUBMODEL_ANIMATION, "trigger-submodel-animation\r\n" "\tActivates a submodel animation trigger for a given ship. Takes 4 to 6 arguments...\r\n" diff --git a/code/parse/sexp.h b/code/parse/sexp.h index 3cf7ab386aa..e59fa4650d3 100644 --- a/code/parse/sexp.h +++ b/code/parse/sexp.h @@ -127,6 +127,7 @@ struct ship_obj; #define OPF_ANIMATION_NAME 96 // Lafiel #define OPF_CONTAINER_VALUE 97 // jg18 - Container data and map container keys #define OPF_DATA_OR_STR_CONTAINER 98 // jg18 - any data, or a container that is accessed via strings +#define OPF_TRANSLATING_SUBSYSTEM 99 // Goober5000 - a translating subsystem // Operand return types #define OPR_NUMBER 1 // returns number @@ -820,6 +821,10 @@ struct ship_obj; #define OP_COPY_CONTAINER (0x0051 | OP_CATEGORY_CHANGE2 | OP_NONCAMPAIGN_FLAG) // jg18 #define OP_APPLY_CONTAINER_FILTER (0x0052 | OP_CATEGORY_CHANGE2 | OP_NONCAMPAIGN_FLAG) // jg18 #define OP_STOP_LOOPING_ANIMATION (0x0053 | OP_CATEGORY_CHANGE2 | OP_NONCAMPAIGN_FLAG) // Lafiel +#define OP_LOCK_TRANSLATING_SUBSYSTEM (0x0054 | OP_CATEGORY_CHANGE2 | OP_NONCAMPAIGN_FLAG) // Goober5000 +#define OP_FREE_TRANSLATING_SUBSYSTEM (0x0055 | OP_CATEGORY_CHANGE2 | OP_NONCAMPAIGN_FLAG) // Goober5000 +#define OP_REVERSE_TRANSLATING_SUBSYSTEM (0x0056 | OP_CATEGORY_CHANGE2 | OP_NONCAMPAIGN_FLAG) // Goober5000 +#define OP_TRANSLATING_SUBSYS_SET_SPEED (0x0057 | OP_CATEGORY_CHANGE2 | OP_NONCAMPAIGN_FLAG) // Goober5000 // defined for AI goals @@ -1125,6 +1130,7 @@ const char *CTEXT(int n); #define SEXP_CHECK_INVALID_SPECIAL_ARG_TYPE -173 #define SEXP_CHECK_INVALID_AWACS_SUBSYS -174 #define SEXP_CHECK_INVALID_ROTATING_SUBSYS -175 +#define SEXP_CHECK_INVALID_TRANSLATING_SUBSYS -176 #define TRAINING_CONTEXT_SPEED (1<<0) diff --git a/fred2/sexp_tree.cpp b/fred2/sexp_tree.cpp index f789ab309c9..54186d46c98 100755 --- a/fred2/sexp_tree.cpp +++ b/fred2/sexp_tree.cpp @@ -3171,6 +3171,7 @@ int sexp_tree::get_default_value(sexp_list_item *item, char *text_buf, int op, i case OPF_SUBSYSTEM: case OPF_AWACS_SUBSYSTEM: case OPF_ROTATING_SUBSYSTEM: + case OPF_TRANSLATING_SUBSYSTEM: case OPF_SUBSYS_OR_GENERIC: str = ""; break; @@ -3301,6 +3302,7 @@ int sexp_tree::query_default_argument_available(int op, int i) case OPF_SUBSYSTEM: case OPF_AWACS_SUBSYSTEM: case OPF_ROTATING_SUBSYSTEM: + case OPF_TRANSLATING_SUBSYSTEM: case OPF_SUBSYSTEM_TYPE: case OPF_DOCKER_POINT: case OPF_DOCKEE_POINT: @@ -3780,6 +3782,7 @@ int sexp_tree::verify_tree(int node, int *bypass) case OPF_SUBSYSTEM: case OPF_AWACS_SUBSYSTEM: case OPF_ROTATING_SUBSYSTEM: + case OPF_TRANSLATING_SUBSYSTEM: if (type2 == SEXP_ATOM_STRING) if (ai_get_subsystem_type(tree_nodes[node].text) == SUBSYSTEM_UNKNOWN) type2 = 0; @@ -5033,6 +5036,7 @@ sexp_list_item *sexp_tree::get_listing_opf(int opf, int parent_node, int arg_ind case OPF_AWACS_SUBSYSTEM: case OPF_ROTATING_SUBSYSTEM: + case OPF_TRANSLATING_SUBSYSTEM: case OPF_SUBSYSTEM: list = get_listing_opf_subsystem(parent_node, arg_index); break; @@ -5727,7 +5731,8 @@ sexp_list_item *sexp_tree::get_listing_opf_wing() #define OPS_BEAM_TURRET 3 #define OPS_AWACS 4 #define OPS_ROTATE 5 -#define OPS_ARMOR 6 +#define OPS_TRANSLATE 6 +#define OPS_ARMOR 7 sexp_list_item *sexp_tree::get_listing_opf_subsystem(int parent_node, int arg_index) { int op, child, sh; @@ -5773,6 +5778,14 @@ sexp_list_item *sexp_tree::get_listing_opf_subsystem(int parent_node, int arg_in special_subsys = OPS_ROTATE; break; + // translating + case OP_LOCK_TRANSLATING_SUBSYSTEM: + case OP_FREE_TRANSLATING_SUBSYSTEM: + case OP_REVERSE_TRANSLATING_SUBSYSTEM: + case OP_TRANSLATING_SUBSYS_SET_SPEED: + special_subsys = OPS_TRANSLATE; + break; + // where we care about capital ship subsystem cargo case OP_CAP_SUBSYS_CARGO_KNOWN_DELAY: special_subsys = OPS_CAP_CARGO; @@ -5909,6 +5922,13 @@ sexp_list_item *sexp_tree::get_listing_opf_subsystem(int parent_node, int arg_in } break; + // translating + case OPS_TRANSLATE: + if (subsys->system_info->flags[Model::Subsystem_Flags::Translates]) { + head.add_data(subsys->system_info->subobj_name); + } + break; + // everything else default: head.add_data(subsys->system_info->subobj_name); From 662d71a57a123120ec9cfa933941157a4e4d4b06 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 25 Jun 2022 00:07:27 -0400 Subject: [PATCH 4/7] add model animation support, based on Lafiel's guidance --- code/model/model.h | 1 + code/model/modelanimation.cpp | 46 +++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/code/model/model.h b/code/model/model.h index d2defddbdfa..d20a1feaef6 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -119,6 +119,7 @@ struct submodel_instance //SMI-Specific movement axis. Only valid in MOVEMENT_TYPE_TRIGGERED. vec3d rotation_axis; + vec3d translation_axis; submodel_instance() { diff --git a/code/model/modelanimation.cpp b/code/model/modelanimation.cpp index ad49829ecbe..60bc1fc5a94 100644 --- a/code/model/modelanimation.cpp +++ b/code/model/modelanimation.cpp @@ -322,6 +322,8 @@ namespace animation { if (!submodel) return; + // rotation ----- + submodel->canonical_prev_orient = submodel->canonical_orient; submodel->canonical_orient = data.orientation; @@ -333,8 +335,19 @@ namespace animation { vm_matrix_to_rot_axis_and_angle(&delta, &deltaAngle, &submodel->rotation_axis); submodel->current_turn_rate = deltaAngle / flFrametime; - //TODO: Once translation is a thing - //m_subsys->submodel_instance_1->offset = data.position; + // translation ----- + + submodel->canonical_prev_offset = submodel->canonical_offset; + submodel->canonical_offset = data.position; + + vec3d delta_vec; + vm_vec_sub(&delta_vec, &submodel->canonical_offset, &submodel->canonical_prev_offset); + + // figure out the sign of the displacement based on whether it is along or against the axis + int sign = (vm_vec_dot(&delta_vec, &submodel->translation_axis) < 0.0f) ? -1 : 1; + + float deltaMag = sign * vm_vec_mag(&delta_vec); + submodel->current_shift_rate = deltaMag / flFrametime; } void ModelAnimationSubmodel::resetPhysicsData(polymodel_instance* pmi) { @@ -361,15 +374,19 @@ namespace animation { if (submodel.second->rotation_type == MOVEMENT_TYPE_NONE) { submodel.second->rotation_type = MOVEMENT_TYPE_TRIGGERED; } + if (submodel.second->translation_type == MOVEMENT_TYPE_NONE) { + submodel.second->translation_type = MOVEMENT_TYPE_TRIGGERED; + } } data.orientation = submodel.first->canonical_orient; - //TODO: Once translation is a thing - //data.position = m_subsys->submodel_instance_1->offset; + data.position = submodel.first->canonical_offset; //In this case, we just initial-type initialized the submodel. Properly set its last frame data as well - if(isInitialType) + if (isInitialType) { submodel.first->canonical_prev_orient = submodel.first->canonical_orient; + submodel.first->canonical_prev_offset = submodel.first->canonical_offset; + } return true; } @@ -481,15 +498,30 @@ namespace animation { if (!submodel) return; + // rotation ----- + submodel->canonical_prev_orient = submodel->canonical_orient; submodel->canonical_orient = data.orientation; + submodel->rotation_axis = sm->rotation_axis; + float angle = 0.0f; vm_closest_angle_to_matrix(&submodel->canonical_orient, &sm->rotation_axis, &angle); - submodel->rotation_axis = sm->rotation_axis; submodel->cur_angle = angle; submodel->turret_idle_angle = angle; + + // translation ----- + + submodel->canonical_prev_offset = submodel->canonical_offset; + submodel->canonical_offset = data.position; + + submodel->translation_axis = sm->translation_axis; + + // figure out the sign of the displacement based on whether it is along or against the axis + int sign = (vm_vec_dot(&submodel->canonical_offset, &submodel->translation_axis) < 0.0f) ? -1 : 1; + + submodel->cur_offset = sign * vm_vec_mag(&submodel->canonical_offset); } @@ -1504,7 +1536,7 @@ namespace animation { {"$Set Angle:", ModelAnimationSegmentSetAngle::parser}, {"$Rotation:", ModelAnimationSegmentRotation::parser}, {"$Axis Rotation:", ModelAnimationSegmentAxisRotation::parser}, - // {"$Translation:", ModelAnimationSegmentTranslation::parser}, + {"$Translation:", ModelAnimationSegmentTranslation::parser}, {"$Sound During:", ModelAnimationSegmentSoundDuring::parser}, {"$Inverse Kinematics:", ModelAnimationSegmentIK::parser} }; From 018363dac6d178f2215a727f68e914dc33acb5c0 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 19 Aug 2022 23:10:32 -0400 Subject: [PATCH 5/7] edit lab flag --- code/lab/dialogs/render_options.cpp | 6 +++--- code/lab/renderer/lab_renderer.cpp | 2 +- code/lab/renderer/lab_renderer.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/code/lab/dialogs/render_options.cpp b/code/lab/dialogs/render_options.cpp index a0c906c7d3e..a6e725a430c 100644 --- a/code/lab/dialogs/render_options.cpp +++ b/code/lab/dialogs/render_options.cpp @@ -20,10 +20,10 @@ void set_show_damage_lightning_flag(Checkbox* caller) { getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowDamageLightning, !value); } -void set_rotate_subsys_flag(Checkbox* caller) { +void set_move_subsys_flag(Checkbox* caller) { auto value = caller->GetChecked(); - getLabManager()->Renderer->setRenderFlag(LabRenderFlag::RotateSubsystems, !value); + getLabManager()->Renderer->setRenderFlag(LabRenderFlag::MoveSubsystems, !value); } void set_post_proc_flag(Checkbox* caller) { @@ -232,7 +232,7 @@ void RenderOptions::open(Button* /*caller*/) { cbp = (Checkbox*)dialogWindow->AddChild(new Checkbox("Show Damage lightning", 2, y, set_show_damage_lightning_flag)); y += cbp->GetHeight() + 2; - cbp = (Checkbox*)dialogWindow->AddChild(new Checkbox("Rotate Subsystems", 2, y, set_rotate_subsys_flag)); + cbp = (Checkbox*)dialogWindow->AddChild(new Checkbox("Rotate/Translate Subsystems", 2, y, set_move_subsys_flag)); y += cbp->GetHeight() + 2; cbp = (Checkbox*)dialogWindow->AddChild(new Checkbox("Hide Post Processing", 2, y, set_post_proc_flag)); diff --git a/code/lab/renderer/lab_renderer.cpp b/code/lab/renderer/lab_renderer.cpp index 2e34ad99bee..d7ae2795063 100644 --- a/code/lab/renderer/lab_renderer.cpp +++ b/code/lab/renderer/lab_renderer.cpp @@ -60,7 +60,7 @@ void LabRenderer::renderModel(float frametime) { PostProcessing_override = renderFlags[LabRenderFlag::HidePostProcessing]; if (obj->type == OBJ_SHIP) { - Ships[obj->instance].flags.set(Ship::Ship_Flags::Subsystem_movement_locked, !renderFlags[LabRenderFlag::RotateSubsystems]); + Ships[obj->instance].flags.set(Ship::Ship_Flags::Subsystem_movement_locked, !renderFlags[LabRenderFlag::MoveSubsystems]); Ships[obj->instance].flags.set(Ship::Ship_Flags::Draw_as_wireframe, renderFlags[LabRenderFlag::ShowWireframe]); Ships[obj->instance].flags.set(Ship::Ship_Flags::Render_full_detail, renderFlags[LabRenderFlag::ShowFullDetail]); Ships[obj->instance].flags.set(Ship::Ship_Flags::Render_without_light, diff --git a/code/lab/renderer/lab_renderer.h b/code/lab/renderer/lab_renderer.h index 846d8a4b511..1f3caa91a42 100644 --- a/code/lab/renderer/lab_renderer.h +++ b/code/lab/renderer/lab_renderer.h @@ -16,7 +16,7 @@ FLAG_LIST(LabRenderFlag) { ModelRotationEnabled, ShowInsignia, ShowDamageLightning, - RotateSubsystems, + MoveSubsystems, HidePostProcessing, NoDiffuseMap, NoGlowMap, From 9116f5de2d9d358887a0b4f1c2cd1d17f3c6c14a Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 25 Aug 2022 23:46:53 -0400 Subject: [PATCH 6/7] improve API --- code/scripting/api/objs/modelinstance.cpp | 7 ++++++- code/scripting/api/objs/subsystem.cpp | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/code/scripting/api/objs/modelinstance.cpp b/code/scripting/api/objs/modelinstance.cpp index fb53db01122..14a31624861 100644 --- a/code/scripting/api/objs/modelinstance.cpp +++ b/code/scripting/api/objs/modelinstance.cpp @@ -148,7 +148,12 @@ ADE_VIRTVAR(Orientation, l_SubmodelInstance, "orientation", "Gets or sets the su return ade_set_args(L, "o", l_Matrix.Set(matrix_h(&smi->canonical_orient))); } -ADE_VIRTVAR(Offset, l_SubmodelInstance, "vector", "Gets or sets the translated submodel instance offset", "vector", "Offset, or zero vector if handle is not valid") +ADE_VIRTVAR(TranslationOffset, + l_SubmodelInstance, + "vector", + "Gets or sets the translated submodel instance offset. This is relative to the existing submodel offset to its parent; a non-translated submodel will have a TranslationOffset of zero.", + "vector", + "Offset, or zero vector if handle is not valid") { submodelinstance_h *smih; vec3d *vec = nullptr; diff --git a/code/scripting/api/objs/subsystem.cpp b/code/scripting/api/objs/subsystem.cpp index 1bcb9241639..f9fccbae499 100644 --- a/code/scripting/api/objs/subsystem.cpp +++ b/code/scripting/api/objs/subsystem.cpp @@ -156,7 +156,12 @@ ADE_VIRTVAR(GunOrientation, l_Subsystem, "orientation", "Orientation of turret g return ade_set_args(L, "o", l_Matrix.Set(matrix_h(&smi->canonical_orient))); } -ADE_VIRTVAR(Offset, l_Subsystem, "vector", "Translated offset of subobject or turret base", "vector", "Offset, or zero vector if handle is not valid") +ADE_VIRTVAR(TranslationOffset, + l_Subsystem, + "vector", + "Gets or sets the translated submodel instance offset of the subsystem or turret base. This is relative to the existing submodel offset to its parent; a non-translated submodel will have a TranslationOffset of zero.", + "vector", + "Offset, or zero vector if handle is not valid") { ship_subsys_h *sso; vec3d *vec = nullptr; From 67729990e3c77b64dea52ed7c1be783d7c5f11c4 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 27 Aug 2022 13:08:53 -0400 Subject: [PATCH 7/7] we don't need to bump the multi version yet --- code/network/multi.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/network/multi.h b/code/network/multi.h index 17d2ead72b2..6808905fbe9 100644 --- a/code/network/multi.h +++ b/code/network/multi.h @@ -71,10 +71,10 @@ class player; // version 55 - 8/28/2021 - Adding multi-compatible animations // version 56 - 8/28/2021 - Fix animations for 22_0 release // version 57 - 6/5/2022 - Upgrade interpolation, fix multiplayer sexp handling, and enable player orders to exceed 16 -// version 58 - ??/??/2022 - Submodel translation +// version 58 - ??/??/?? - Submodel translation and a fixed multi_pack_unpack_subsystem_list // STANDALONE_ONLY -#define MULTI_FS_SERVER_VERSION 58 +#define MULTI_FS_SERVER_VERSION 57 #define MULTI_FS_SERVER_COMPATIBLE_VERSION MULTI_FS_SERVER_VERSION