#include "modelreplace.h" #include "model/model.h" #include "model/modelinterp.h" #include "cfile/cfile.h" #include #include #include #include #include static SCP_unordered_map, SCP_string_lcase_hash, SCP_string_lcase_equal_to> virtual_pofs; static SCP_unordered_map()>> virtual_pof_operations = { {"$Add Subobject:", &std::make_unique }, {"$Add Turret:", &std::make_unique }, {"$Add Engine:", &std::make_unique }, {"$Add Glowpoint:", &std::make_unique }, {"$Add Weapon Bank:", &std::make_unique }, {"$Add Dock Point:", &std::make_unique }, {"$Add Path:", &std::make_unique }, {"$Rename Subobjects:", &std::make_unique }, {"$Set Subsystem Data:", &std::make_unique }, {"$Set Subobject Data:", &std::make_unique }, {"$Set Header Data:", &std::make_unique } }; /* * forward declares for internal modelread functions */ extern modelread_status read_model_file(polymodel* pm, const char* filename, ErrorType error_type, model_read_deferred_tasks& deferredTasks, model_parse_depth depth = {}); extern void create_family_tree(polymodel* obj); extern void model_calc_bound_box(vec3d* box, const vec3d* big_mn, const vec3d* big_mx); /* * Caching base-loaded POFs, as well has providing automatic deallocation for loaded models */ static class VirtualPOFBuildCache { public: class polymodel_holder; private: SCP_unordered_map, SCP_string_lcase_hash, SCP_string_lcase_equal_to> cache; public: class polymodel_holder { polymodel* _pm; model_read_deferred_tasks _deferred; SCP_set keepTextures{}, keepGlowbanks{}, keepSM{}; bool needs_emplace = false; bool _loaded = false; friend class VirtualPOFBuildCache; public: polymodel_holder(const SCP_string& pof_name, const model_parse_depth& depth) : _pm(new polymodel()), _deferred() { auto status = read_model_file(_pm, pof_name.c_str(), ErrorType::WARNING, _deferred, depth); create_family_tree(_pm); if (status == modelread_status::SUCCESS_REAL) needs_emplace = true; _loaded = (status != modelread_status::FAIL); } //Don't copy, just move polymodel_holder(const polymodel_holder&) = delete; polymodel_holder& operator=(const polymodel_holder&) = delete; ~polymodel_holder() { if (_pm == nullptr) return; for (int sm : keepSM) _pm->submodel[sm].bsp_data = nullptr; model_page_out_textures(_pm, true, keepTextures, keepGlowbanks); model_free(_pm); } inline const model_read_deferred_tasks& deferred() const { return _deferred; } inline const polymodel* pm() const { return _pm; } inline bool loaded() const { return _loaded; } inline void keepTexture(int texture) { keepTextures.emplace(texture); } inline void keepGlowbank(int gb) { keepGlowbanks.emplace(gb); } inline void keepBSPData(int sm) { keepSM.emplace(sm); } }; const std::shared_ptr operator()(const SCP_string& pof_name, model_parse_depth& depth) { auto vp_it = virtual_pofs.find(pof_name); auto it = cache.find(pof_name); //Don't load from cache if it's a virtual pof (always reload these) or we don't have it cached if ((vp_it != virtual_pofs.end() && (int)vp_it->second.size() > depth[pof_name]) || it == cache.end()) { auto pmh = std::make_shared(pof_name, depth); if(pmh->needs_emplace) cache.emplace(pof_name, pmh); return pmh; } return it->second; } void clear() { cache.clear(); } } virtual_pof_build_cache; // Returns nullptr, with a warning, if the POF could not be loaded static std::shared_ptr load_source_pof(const SCP_string& pof_name, model_parse_depth& depth, const VirtualPOFDefinition& virtualPof) { auto pmh = virtual_pof_build_cache(pof_name, depth); if (!pmh->loaded()) { Warning(LOCATION, "Could not load POF %s for virtual POF %s. Returning original POF.", pof_name.c_str(), virtualPof.name.c_str()); return nullptr; } return pmh; } // General Functions and external code paths bool model_exists(const SCP_string& filename) { auto it = virtual_pofs.find(filename); if (it != virtual_pofs.end() && !it->second.empty()) { return true; } return cf_exists_full(filename.c_str(), CF_TYPE_MODELS); } std::optional read_virtual_model_file(polymodel* pm, const SCP_string& filename, model_parse_depth depth, ErrorType error_type, model_read_deferred_tasks& deferredTasks) { auto virtual_pof_it = virtual_pofs.find(filename); //We don't have a virtual pof if(virtual_pof_it == virtual_pofs.end()) return std::nullopt; //We have one, but we're already past it and are processing whatever it overwrote int& depthLocal = depth[filename]; if ((int)virtual_pof_it->second.size() <= depthLocal) return std::nullopt; const auto& virtual_pof = virtual_pof_it->second[depthLocal]; depthLocal++; if (read_model_file(pm, virtual_pof.basePOF.c_str(), error_type, deferredTasks, depth) == modelread_status::FAIL) return modelread_status::FAIL; for (const auto& operation : virtual_pof.operationList) operation->process(pm, deferredTasks, depth, virtual_pof); return modelread_status::SUCCESS_VIRTUAL; } void virtual_pof_purge_cache() { virtual_pof_build_cache.clear(); } // Parsing function static void parse_virtual_pof() { SCP_string name; stuff_string(name, F_NAME); auto& virtual_pof_list = virtual_pofs[name]; virtual_pof_list.emplace_back(); VirtualPOFDefinition& toFill = virtual_pof_list.back(); toFill.name = name; required_string("+Base POF:"); stuff_string(toFill.basePOF, F_FILESPEC); do { ignore_white_space(); char operation_type[NAME_LENGTH]; stuff_string(operation_type, F_NAME, NAME_LENGTH); auto operation_parser = virtual_pof_operations.find(operation_type); if (operation_parser != virtual_pof_operations.end()) toFill.operationList.emplace_back(operation_parser->second()); else { error_display(1, "Unknown operation type %s in virtual POF %s!", operation_type, name.c_str()); virtual_pof_list.pop_back(); skip_to_start_of_string_either("$POF:", "#End"); return; } } while (optional_string_either("$POF:", "#End", false) == -1); } static void parse_virtual_pof_table(const char* filename) { try { read_file_text(filename, CF_TYPE_TABLES); reset_parse(); required_string("#Virtual POFs"); while (optional_string("$POF:")) parse_virtual_pof(); required_string("#End"); } catch (const parse::ParseException& e) { mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", filename, e.what())); return; } } void virtual_pof_init() { if (cf_exists_full("virtual_pofs.tbl", CF_TYPE_TABLES)) parse_virtual_pof_table("virtual_pofs.tbl"); parse_modular_table(NOX("*-pof.tbm"), parse_virtual_pof_table); } // Internal helper functions template inline void object_copy_including_array_member_inner(const T& item, T& result, int size, member_t T::* ptm) { if constexpr (is_smart_pointer_v>) result.*ptm = make_shared::element_type[]>(size); else if constexpr (vmalloc) result.*ptm = static_cast(vm_malloc(sizeof(std::remove_pointer_t) * size)); else result.*ptm = new std::remove_pointer_t[size]; for (int i = 0; i < size; i++) (result.*ptm)[i] = (item.*ptm)[i]; } template inline void object_copy_including_array_member_inner(const T& item, T& result, int size, member_t_0 T::* ptm, member_t_1 T::* ptm1, member_t T::*... ptms) { object_copy_including_array_member_inner(item, result, size, ptm); object_copy_including_array_member_inner(item, result, size, ptm1, ptms...); } template T object_copy_including_array_member(const T& item, int T::* size, member_t T::*... ptm) { T result{ item }; object_copy_including_array_member_inner(item, result, result.*size, ptm...); return result; } template int reallocate_and_copy_array(std::shared_ptr& array, int& size, size_t to_add) { //Make sure to keep old data std::shared_ptr oldArray = array; int size_before = size; //Realloc new submodel array of proper size size += static_cast(to_add); array = make_shared(size); //Copy over old data. Pointers in the struct can still point to old members, we will just delete the outer bsp_info array for (int i = 0; i < size_before; i++) array[i] = std::move(oldArray[i]); return size_before; } #define REPLACE_IF_EQ(data) { auto iter = replace.find(data); if (iter != replace.end()) (data) = iter->second; } #define REPLACE_IF_STRIEQ(data) { auto iter = replace.find(data); if (iter != replace.end()) strncpy_s(data, iter->second.c_str(), iter->second.size()); } //Generates one function for replacing data in a type, which is a map entry of which the key may be replaced. Takes an rvalue reference, used for making a copy and modifying the temporary to then assign it somewhere #define CHANGE_HELPER_MAP_KEY(name, intype, argtype) template static typename std::enable_if>::value, intype>::type name(intype&& pass, map_t replace){ \ const auto it = replace.find(pass.first); \ intype input = { (it == replace.end() ? pass.first : it->second), std::move(pass.second) }; #define CHANGE_HELPER_MAP_KEY_END return input; } //Generates two functions for replacing data in a type. One that takes an rvalue reference, used for making a copy and modifying the temporary to then assign it somewhere, and one which takes an lvalue reference for modifying in-place #define CHANGE_HELPER(name, intype, argtype) template static typename std::enable_if>::value>::type name(intype& input, map_t replace); \ template inline static typename std::enable_if>::value, intype>::type name(intype&& input, map_t replace){ \ name(input, replace); \ return std::move(input); \ } \ template static typename std::enable_if>::value>::type name(intype& input, map_t replace) { #define CHANGE_HELPER_END } //Change submodel numbers in a submodel CHANGE_HELPER(change_submodel_numbers, bsp_info, int) for (auto& detail : input.details) REPLACE_IF_EQ(detail); REPLACE_IF_EQ(input.first_child); REPLACE_IF_EQ(input.prev_form); for (auto& debris : input.live_debris) REPLACE_IF_EQ(debris); REPLACE_IF_EQ(input.look_at_submodel); REPLACE_IF_EQ(input.next_form); REPLACE_IF_EQ(input.next_sibling); REPLACE_IF_EQ(input.parent); CHANGE_HELPER_END //Change submodel numbers in a subsystem definition CHANGE_HELPER(change_submodel_numbers, model_read_deferred_tasks::model_subsystem_pair, int) REPLACE_IF_EQ(input.second.subobj_nr); CHANGE_HELPER_END //Change submodel numbers in a glowpoint bank definition CHANGE_HELPER(change_submodel_numbers, glow_point_bank, int) REPLACE_IF_EQ(input.submodel_parent); CHANGE_HELPER_END //Change submodel numbers in a turret definition CHANGE_HELPER_MAP_KEY(change_submodel_numbers, model_read_deferred_tasks::weapon_subsystem_pair, int) REPLACE_IF_EQ(input.second.gun_subobj_nr); CHANGE_HELPER_MAP_KEY_END //Change submodel name in a submodel CHANGE_HELPER(change_submodel_name, bsp_info, SCP_string) REPLACE_IF_STRIEQ(input.name); REPLACE_IF_STRIEQ(input.lod_name); CHANGE_HELPER_END //Change submodel name in a subsystem definition CHANGE_HELPER_MAP_KEY(change_submodel_name, model_read_deferred_tasks::model_subsystem_pair, SCP_string) CHANGE_HELPER_MAP_KEY_END //Change subsystem name in an engine subsys definition CHANGE_HELPER(change_submodel_name, model_read_deferred_tasks::engine_subsystem_pair, SCP_string) REPLACE_IF_EQ(input.second.subsystem_name) CHANGE_HELPER_END //Change engine numbers in an engine subsys definition CHANGE_HELPER_MAP_KEY(change_engine_numbers, model_read_deferred_tasks::engine_subsystem_pair, int) CHANGE_HELPER_MAP_KEY_END #undef CHANGE_HELPER #undef CHANGE_HELPER_END #undef CHANGE_HELPER_MAP_KEY #undef CHANGE_HELPER_MAP_KEY_END #undef REPLACE_IF_EQ #undef REPLACE_IF_STRIEQ // Actual replacement operations VirtualPOFOperationAddSubmodel::VirtualPOFOperationAddSubmodel() { required_string("+POF to Add:"); stuff_string(appendingPOF, F_FILESPEC); required_string("+Source Subobject:"); stuff_string(subobjNameSrc, F_NAME); if (optional_string("+Copy Children:")) { stuff_boolean(©Children); } if (optional_string("+Copy Turrets:")) { stuff_boolean(©Turrets); } if (optional_string("+Copy Glowpoints:")) { stuff_boolean(©Glowpoints); } if (optional_string("$Rename Subobjects:")) { rename = std::make_unique(); } required_string("+Destination Subobject:"); stuff_string(subobjNameDest, F_NAME); } void VirtualPOFOperationAddSubmodel::process(polymodel* pm, model_read_deferred_tasks& deferredTasks, model_parse_depth depth, const VirtualPOFDefinition& virtualPof) const { auto appendingPMholder = load_source_pof(appendingPOF, depth, virtualPof); if (appendingPMholder == nullptr) return; const model_read_deferred_tasks& appendingSubsys = appendingPMholder->deferred(); const polymodel* appendingPM = appendingPMholder->pm(); int src_subobj_no = model_find_submodel_index(appendingPM, subobjNameSrc.c_str()); int dest_subobj_no = model_find_submodel_index(pm, subobjNameDest.c_str()); if (src_subobj_no >= 0 && dest_subobj_no >= 0) { create_family_tree(pm); const VirtualPOFNameReplacementMap& renameMap = (rename == nullptr ? VirtualPOFNameReplacementMap{} : rename->replacements); SCP_vector to_copy_submodels; bool has_name_collision = false; if (copyChildren) { model_iterate_submodel_tree(appendingPM, src_subobj_no, [&to_copy_submodels, &has_name_collision, &renameMap, pm, appendingPM](int submodel, int /*level*/, bool /*isLeaf*/) { to_copy_submodels.emplace_back(submodel); auto replace_with_name_it = renameMap.find(appendingPM->submodel[submodel].name); if(model_find_submodel_index(pm, replace_with_name_it == renameMap.end() ? appendingPM->submodel[submodel].name : replace_with_name_it->second.c_str()) != -1) has_name_collision = true; }); } else { to_copy_submodels.emplace_back(src_subobj_no); auto replace_with_name_it = renameMap.find(appendingPM->submodel[src_subobj_no].name); if(model_find_submodel_index(pm, replace_with_name_it == renameMap.end() ? appendingPM->submodel[src_subobj_no].name : replace_with_name_it->second.c_str()) != -1) has_name_collision = true; } if (!has_name_collision) { //Assign new texture indices before changing anything, so that running out of textures leaves the model intact SCP_vector> texturesUsed; SCP_map textureIDReplace; int new_n_textures = pm->n_textures; for (int submodel : to_copy_submodels) { texturesUsed.emplace_back(model_get_textures_used(appendingPM, submodel)); for (int textureID : texturesUsed.back()) { //Flat polygons are untextured if (textureID < 0) continue; if (textureIDReplace.find(textureID) == textureIDReplace.end()) textureIDReplace.emplace(textureID, new_n_textures++); } } if (new_n_textures > MAX_MODEL_TEXTURES) { Warning(LOCATION, "Failed to add submodel %s of POF %s to virtual POF %s, combined POF has too many (over %d) textures.", subobjNameSrc.c_str(), appendingPOF.c_str(), virtualPof.name.c_str(), MAX_MODEL_TEXTURES); return; } pm->n_textures = new_n_textures; int old_n_submodel = reallocate_and_copy_array(pm->submodel, pm->n_models, to_copy_submodels.size()); SCP_unordered_map replaceSubobjNo; for (int i = 0; i < (int)to_copy_submodels.size(); i++) replaceSubobjNo.emplace(to_copy_submodels[i], i + old_n_submodel); //Copy subsystem definitions for (int i = 0; i < (int)to_copy_submodels.size(); i++) { auto it = appendingSubsys.model_subsystems.find(appendingPM->submodel[to_copy_submodels[i]].name); if (it != appendingSubsys.model_subsystems.end()) { deferredTasks.model_subsystems.emplace( change_submodel_name(change_submodel_numbers(model_read_deferred_tasks::model_subsystem_pair(*it), replaceSubobjNo), renameMap) ); } } int deltaDepth = (pm->submodel[dest_subobj_no].depth + 1) - appendingPM->submodel[src_subobj_no].depth; //Copy over new data. This one needs to be fully free'd afterwards, so make sure to nullptr the respective pointers before freeing later for (int i = 0; i < (int)to_copy_submodels.size(); i++) { auto& newSubmodel = pm->submodel[i + old_n_submodel]; newSubmodel = change_submodel_name(change_submodel_numbers(bsp_info(appendingPM->submodel[to_copy_submodels[i]]), replaceSubobjNo), renameMap); //Set new Depth newSubmodel.depth += deltaDepth; //Store texture replacement indices for (int textureID : texturesUsed[i]) { if (textureID < 0) continue; deferredTasks.texture_replacements[i + old_n_submodel].replacementIds[textureID] = textureIDReplace.at(textureID); } //Clear old pointer to data appendingPMholder->keepBSPData(to_copy_submodels[i]); } //Register as next child to our destination pm->submodel[dest_subobj_no].num_children++; int last_sibling = pm->submodel[dest_subobj_no].first_child; int last_true_sibling = -1; while (last_sibling >= 0) { last_true_sibling = last_sibling; last_sibling = pm->submodel[last_sibling].next_sibling; } if(last_true_sibling == -1) pm->submodel[dest_subobj_no].first_child = old_n_submodel; else pm->submodel[last_true_sibling].next_sibling = old_n_submodel; //The selected submodel will always be first, and thus get the first id pm->submodel[old_n_submodel].next_sibling = -1; //Our old submodel might've had a sibling. Not anymore. pm->submodel[old_n_submodel].parent = dest_subobj_no; if (!copyChildren) { //Clear children if needed pm->submodel[old_n_submodel].num_children = 0; pm->submodel[old_n_submodel].first_child = -1; } //Actually copy textures for (const auto& usedTexture : textureIDReplace) { pm->maps[usedTexture.second] = appendingPM->maps[usedTexture.first]; appendingPMholder->keepTexture(usedTexture.first); } if (copyTurrets) { //Copy turrets for (const auto& turretSubsys : appendingSubsys.weapons_subsystems) { //Don't copy turrets that aren't fully contained in data to be copied if (std::find(to_copy_submodels.begin(), to_copy_submodels.end(), turretSubsys.first) == to_copy_submodels.end()) continue; if (turretSubsys.second.gun_subobj_nr != turretSubsys.first && std::find(to_copy_submodels.begin(), to_copy_submodels.end(), turretSubsys.second.gun_subobj_nr) == to_copy_submodels.end()) continue; deferredTasks.weapons_subsystems.emplace(change_submodel_numbers(model_read_deferred_tasks::weapon_subsystem_pair(turretSubsys), replaceSubobjNo)); } } if (copyGlowpoints) { SCP_vector glowpointbanks; for (int i = 0; i < appendingPM->n_glow_point_banks; i++) { const glow_point_bank* gpb = &appendingPM->glow_point_banks[i]; //Don't copy glowpoints that aren't fully contained in data to be copied if (std::find(to_copy_submodels.begin(), to_copy_submodels.end(), gpb->submodel_parent) == to_copy_submodels.end()) continue; appendingPMholder->keepGlowbank(i); glowpointbanks.emplace_back(std::move(gpb)); } int insertFrom = reallocate_and_copy_array(pm->glow_point_banks, pm->n_glow_point_banks, glowpointbanks.size()); for (const glow_point_bank* gpb : glowpointbanks) { pm->glow_point_banks[insertFrom] = object_copy_including_array_member(*gpb, &glow_point_bank::num_points, &glow_point_bank::points); change_submodel_numbers(pm->glow_point_banks[insertFrom], replaceSubobjNo); insertFrom++; } } } else { Warning(LOCATION, "Failed to add submodel %s of POF %s to virtual POF %s, original POF already has a subsystem with the same name as was supposed to be added.", subobjNameSrc.c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); } } else { Warning(LOCATION, "Failed to add submodel %s of POF %s to virtual POF %s, specified subobject was not found. Returning original POF", subobjNameSrc.c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); } } VirtualPOFOperationAddTurret::VirtualPOFOperationAddTurret() { required_string("+POF to Add:"); stuff_string(appendingPOF, F_FILESPEC); required_string("+Source Turret:"); stuff_string(baseNameSrc, F_NAME); required_string("+Destination Turret:"); stuff_string(baseNameDest, F_NAME); if (optional_string("+Destination Barrel:")) { SCP_string barrelDest; stuff_string(barrelDest, F_NAME); barrelNameDest = std::move(barrelDest); } } void VirtualPOFOperationAddTurret::process(polymodel* pm, model_read_deferred_tasks& deferredTasks, model_parse_depth depth, const VirtualPOFDefinition& virtualPof) const { auto appendingPM = load_source_pof(appendingPOF, depth, virtualPof); if (appendingPM == nullptr) return; SCP_unordered_map replaceSubmodelNo; int base_src_subobj_no = model_find_submodel_index(appendingPM->pm(), baseNameSrc.c_str()); int base_dest_subobj_no = model_find_submodel_index(pm, baseNameDest.c_str()); replaceSubmodelNo.emplace(base_src_subobj_no, base_dest_subobj_no); auto it = appendingPM->deferred().weapons_subsystems.find(base_src_subobj_no); if (base_src_subobj_no == -1 || base_dest_subobj_no == -1 || it == appendingPM->deferred().weapons_subsystems.end()) { Warning(LOCATION, "Failed to find turret base %s of POF %s for virtual POF %s. Returning original POF.", baseNameSrc.c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); return; } if (it->second.gun_subobj_nr != base_src_subobj_no) { int gun_dest_subobj_no = -1; if (barrelNameDest) { gun_dest_subobj_no = model_find_submodel_index(pm, barrelNameDest->c_str()); } if (gun_dest_subobj_no == -1) { Warning(LOCATION, "Failed to find turret barrel of turret %s of POF %s for virtual POF %s. Returning original POF.", baseNameDest.c_str(), pm->filename, virtualPof.name.c_str()); return; } replaceSubmodelNo.emplace(it->second.gun_subobj_nr, gun_dest_subobj_no); pm->submodel[gun_dest_subobj_no].rotation_type = MOVEMENT_TYPE_TURRET; } pm->submodel[base_dest_subobj_no].rotation_type = MOVEMENT_TYPE_TURRET; deferredTasks.weapons_subsystems.emplace(change_submodel_numbers(model_read_deferred_tasks::weapon_subsystem_pair(*it), replaceSubmodelNo)); } VirtualPOFOperationAddEngine::VirtualPOFOperationAddEngine() { required_string("+POF to Add:"); stuff_string(appendingPOF, F_FILESPEC); if (required_string_either("+Source Engine:", "+Source Engine Subsystem:") == 0) { Mp += 15; int engineNr; stuff_int(&engineNr); sourceId = engineNr - 1; } else { Mp += 25; SCP_string engineSubsys; stuff_string(engineSubsys, F_NAME); sourceId = std::move(engineSubsys); } if (optional_string("+Move Engine:")) { vec3d offset; stuff_vec3d(&offset); moveEngine = std::move(offset); } if (optional_string("+Destination Subsystem:")) { SCP_string destSubsys; stuff_string(destSubsys, F_NAME); renameSubsystem = std::move(destSubsys); } } void VirtualPOFOperationAddEngine::process(polymodel* pm, model_read_deferred_tasks& deferredTasks, model_parse_depth depth, const VirtualPOFDefinition& virtualPof) const { auto appendingPM = load_source_pof(appendingPOF, depth, virtualPof); if (appendingPM == nullptr) return; const auto& engineSubsysMap = appendingPM->deferred().engine_subsystems; int engineNumber; std::optional subsystemName = std::nullopt; typename std::remove_reference::type::const_iterator it = engineSubsysMap.cend(); if (std::holds_alternative(sourceId)) { engineNumber = std::get(sourceId); it = engineSubsysMap.find(engineNumber); if (it != engineSubsysMap.cend()) subsystemName = it->second.subsystem_name; } else { subsystemName = std::get(sourceId); auto localit = engineSubsysMap.cbegin(); do { if (subsystem_stricmp(localit->second.subsystem_name.c_str(), subsystemName->c_str()) == 0) { if (it != engineSubsysMap.cend()) { Warning(LOCATION, "Engine subsystem %s of POF %s for virtual POF %s is ambiguous. Use engine number instead. Returning original POF.", subsystemName->c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); return; } it = localit; } } while (++localit != engineSubsysMap.cend()); if (it == engineSubsysMap.cend()) { Warning(LOCATION, "Failed to find engine subsystem %s of POF %s for virtual POF %s. Returning original POF.", subsystemName->c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); return; } engineNumber = it->first; } int newEngineNumber = reallocate_and_copy_array(pm->thrusters, pm->n_thrusters, 1); const SCP_unordered_map thrusterReplacementMap{ {engineNumber, newEngineNumber} }; pm->thrusters[newEngineNumber] = object_copy_including_array_member(appendingPM->pm()->thrusters[engineNumber], &thruster_bank::num_points, &thruster_bank::points); if (moveEngine) { const vec3d& offset = *moveEngine; for (int i = 0; i < pm->thrusters[newEngineNumber].num_points; i++) pm->thrusters[newEngineNumber].points[i].pnt += offset; } if (subsystemName) { if (renameSubsystem) { const VirtualPOFNameReplacementMap thrusterSubsysRenameMap{ {*subsystemName, *renameSubsystem} }; deferredTasks.engine_subsystems.emplace(change_submodel_name( change_engine_numbers(model_read_deferred_tasks::engine_subsystem_pair(*it), thrusterReplacementMap), thrusterSubsysRenameMap)); } else { deferredTasks.engine_subsystems.emplace(change_engine_numbers(model_read_deferred_tasks::engine_subsystem_pair(*it), thrusterReplacementMap)); } } } VirtualPOFOperationAddGlowpoint::VirtualPOFOperationAddGlowpoint() { required_string("+POF to Add:"); stuff_string(appendingPOF, F_FILESPEC); required_string("+Source Glowpoint:"); stuff_int(&sourceId); --sourceId; if (optional_string("+Move Glowpoint:")) { vec3d offset; stuff_vec3d(&offset); moveGlowpoint = std::move(offset); } required_string("+Destination Submodel:"); stuff_string(renameSubmodel, F_NAME); } void VirtualPOFOperationAddGlowpoint::process(polymodel* pm, model_read_deferred_tasks& /*deferredTasks*/, model_parse_depth depth, const VirtualPOFDefinition& virtualPof) const { auto appendingPM = load_source_pof(appendingPOF, depth, virtualPof); if (appendingPM == nullptr) return; int dest_subobj_no = model_find_submodel_index(pm, renameSubmodel.c_str()); if (dest_subobj_no < 0) { Warning(LOCATION, "Failed to find submodel %s on virtual POF %s. Returning original POF", renameSubmodel.c_str(), virtualPof.name.c_str()); return; } int newGPNumber = reallocate_and_copy_array(pm->glow_point_banks, pm->n_glow_point_banks, 1); pm->glow_point_banks[newGPNumber] = object_copy_including_array_member(appendingPM->pm()->glow_point_banks[sourceId], &glow_point_bank::num_points, &glow_point_bank::points); pm->glow_point_banks[newGPNumber].submodel_parent = dest_subobj_no; appendingPM->keepGlowbank(sourceId); if (moveGlowpoint) { const vec3d& offset = *moveGlowpoint; for (int i = 0; i < pm->glow_point_banks[newGPNumber].num_points; i++) pm->glow_point_banks[newGPNumber].points[i].pnt += offset; } } VirtualPOFOperationAddSpecialSubsystem::VirtualPOFOperationAddSpecialSubsystem() { required_string("+POF to Add:"); stuff_string(appendingPOF, F_FILESPEC); required_string("+Source Subsystem:"); stuff_string(sourceSubsystem, F_NAME); if (optional_string("+Destination Subsystem:")) { SCP_string dest; stuff_string(dest, F_NAME); renameSubsystem = std::move(dest); } } void VirtualPOFOperationAddSpecialSubsystem::process(polymodel* /*pm*/, model_read_deferred_tasks& deferredTasks, model_parse_depth depth, const VirtualPOFDefinition& virtualPof) const { auto appendingPM = load_source_pof(appendingPOF, depth, virtualPof); if (appendingPM == nullptr) return; const auto& subsystems = appendingPM->deferred().model_subsystems; auto it = subsystems.find(sourceSubsystem); if (it == subsystems.end()) { Warning(LOCATION, "Failed to find subsystem %s on POF %s for virtual POF %s. Returning original POF", sourceSubsystem.c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); return; } if (it->second.subobj_nr >= 0) { Warning(LOCATION, "Subsystem %s on POF %s for virtual POF %s is a modelled subsystem and not a special point. Returning original POF", sourceSubsystem.c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); return; } const SCP_string& targetName = renameSubsystem ? *renameSubsystem : sourceSubsystem; if (deferredTasks.model_subsystems.find(targetName) != deferredTasks.model_subsystems.end()) { Warning(LOCATION, "Subsystem %s already exists on virtual POF %s! Returning original POF", targetName.c_str(), virtualPof.name.c_str()); return; } if(renameSubsystem) deferredTasks.model_subsystems.emplace(change_submodel_name(model_read_deferred_tasks::model_subsystem_pair(*it), VirtualPOFNameReplacementMap{ {sourceSubsystem, *renameSubsystem} })); else deferredTasks.model_subsystems.emplace(*it); } VirtualPOFOperationAddWeapons::VirtualPOFOperationAddWeapons() { required_string("+POF to Add:"); stuff_string(appendingPOF, F_FILESPEC); required_string("+Type:"); primary = required_string_either("Primary", "Secondary") == 0; required_string("+Source Weapon Bank:"); stuff_int(&sourcebank); if (optional_string("+Destination Weapon Bank:")) stuff_int(&destbank); } void VirtualPOFOperationAddWeapons::process(polymodel* pm, model_read_deferred_tasks& /*deferredTasks*/, model_parse_depth depth, const VirtualPOFDefinition& virtualPof) const { auto appendingPM = load_source_pof(appendingPOF, depth, virtualPof); if (appendingPM == nullptr) return; auto& banks = primary ? pm->gun_banks : pm->missile_banks; int& n_banks = primary ? pm->n_guns : pm->n_missiles; const auto& banks_src = primary ? appendingPM->pm()->gun_banks : appendingPM->pm()->missile_banks; const int& n_banks_src = primary ? appendingPM->pm()->n_guns : appendingPM->pm()->n_missiles; const int& n_banks_max = primary ? MAX_SHIP_PRIMARY_BANKS : MAX_SHIP_SECONDARY_BANKS; if (sourcebank < 0 || sourcebank >= n_banks_src) { Warning(LOCATION, "Source bank %d on POF %s for virtual POF %s does not exist. Returning original POF", sourcebank, appendingPOF.c_str(), virtualPof.name.c_str()); return; } int actual_destbank = destbank; if (destbank < 0) { //Add if (n_banks >= n_banks_max) { Warning(LOCATION, "No space for additional destination bank on POF %s for virtual POF %s. Returning original POF", pm->filename, virtualPof.name.c_str()); return; } actual_destbank = reallocate_and_copy_array(banks, n_banks, 1); } else { //Replace if (destbank >= n_banks) { Warning(LOCATION, "Destination bank %d on POF %s for virtual POF %s does not exist. Returning original POF", destbank, pm->filename, virtualPof.name.c_str()); return; } } banks[actual_destbank] = object_copy_including_array_member(banks_src[sourcebank], &w_bank::num_slots, &w_bank::pnt, &w_bank::norm, &w_bank::external_model_angle_offset); } VirtualPOFOperationAddDockPoint::VirtualPOFOperationAddDockPoint() { required_string("+POF to Add:"); stuff_string(appendingPOF, F_FILESPEC); required_string("+Source Dock Point:"); stuff_string(sourcedock, F_NAME); if (optional_string("+Rename Dock Point:")) { SCP_string name; stuff_string(name, F_NAME); renameDock = std::move(name); } while (optional_string("+Rename Path:")) { SCP_string from, to; required_string("+From:"); stuff_string(from, F_NAME); required_string("+To:"); stuff_string(to, F_NAME); renamePaths.emplace(std::move(from), std::move(to)); } if (optional_string("+Parent Submodel:")) { SCP_string name; stuff_string(name, F_NAME); targetParentSubsystem = std::move(name); } } void VirtualPOFOperationAddDockPoint::process(polymodel* pm, model_read_deferred_tasks& /*deferredTasks*/, model_parse_depth depth, const VirtualPOFDefinition& virtualPof) const { auto appendingPMholder = load_source_pof(appendingPOF, depth, virtualPof); if (appendingPMholder == nullptr) return; const polymodel* appendingPM = appendingPMholder->pm(); int dockpoint = model_find_dock_name_index(appendingPM, sourcedock.c_str()); if (dockpoint < 0) { Warning(LOCATION, "Could not find dockpoint %s on POF %s for virtual POF %s. Returning original POF", sourcedock.c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); return; } const SCP_string& targetName = renameDock ? *renameDock : sourcedock; if (model_find_dock_name_index(pm, targetName.c_str()) >= 0) { Warning(LOCATION, "Dockpoint %s already exists on POF %s for virtual POF %s. Returning original POF", targetName.c_str(), pm->filename, virtualPof.name.c_str()); return; } if (appendingPM->docking_bays[dockpoint].parent_submodel >= 0 && !targetParentSubsystem) { Warning(LOCATION, "Dockpoint %s must have a parent submodel specified for virtual POF %s. Returning original POF", sourcedock.c_str(), virtualPof.name.c_str()); return; } int submodel_index = -1; if (targetParentSubsystem) { submodel_index = model_find_submodel_index(pm, targetParentSubsystem->c_str()); if (submodel_index < 0) { Warning(LOCATION, "Submodel %s does not exist on POF %s for virtual POF %s. Returning original POF", targetParentSubsystem->c_str(), pm->filename, virtualPof.name.c_str()); return; } } for (int i = 0; i < appendingPM->docking_bays[dockpoint].num_spline_paths; i++) { const auto& originalName = appendingPM->paths[appendingPM->docking_bays[dockpoint].splines[i]].name; auto renamed = renamePaths.find(originalName); const SCP_string& targetSplineName = renamed == renamePaths.end() ? originalName : renamed->second; for (int j = 0; j < pm->n_paths; j++) { if (stricmp(targetSplineName.c_str(), pm->paths[j].name) == 0) { Warning(LOCATION, "Path %s already exists on POF %s for virtual POF %s. Returning original POF", targetSplineName.c_str(), pm->filename, virtualPof.name.c_str()); return; } } } int destdock = reallocate_and_copy_array(pm->docking_bays, pm->n_docks, 1); pm->docking_bays[destdock] = object_copy_including_array_member(appendingPM->docking_bays[dockpoint], &dock_bay::num_spline_paths, &dock_bay::splines); int splinefrom = reallocate_and_copy_array(pm->paths, pm->n_paths, pm->docking_bays[destdock].num_spline_paths); for (int i = 0; i < pm->docking_bays[destdock].num_spline_paths; i++) { pm->paths[i + splinefrom] = object_copy_including_array_member(appendingPM->paths[appendingPM->docking_bays[dockpoint].splines[i]], &model_path::nverts, &model_path::verts); if (targetParentSubsystem) { strcpy_s(pm->paths[i + splinefrom].parent_name, targetParentSubsystem->c_str()); pm->paths[i + splinefrom].parent_submodel = submodel_index; } else { //Make sure that the path's parent name is cleared in this case pm->paths[i + splinefrom].parent_name[0] = '\0'; } auto renamed = renamePaths.find(pm->paths[i + splinefrom].name); if (renamed != renamePaths.end()) { strcpy_s(pm->paths[i + splinefrom].name, renamed->second.c_str()); } pm->docking_bays[destdock].splines[i] = i + splinefrom; } if (targetParentSubsystem) { pm->docking_bays[destdock].parent_submodel = submodel_index; } if (renameDock) { strcpy_s(pm->docking_bays[destdock].name, renameDock->c_str()); // the name has changed, so the type_flags should be regenerated; // c.f. parsing ID_DOCK in modelread.cpp if ( !strnicmp(pm->docking_bays[destdock].name, "cargo", 5) ) pm->docking_bays[destdock].type_flags = DOCK_TYPE_CARGO; else pm->docking_bays[destdock].type_flags = (DOCK_TYPE_REARM | DOCK_TYPE_GENERIC); } } VirtualPOFOperationAddPath::VirtualPOFOperationAddPath() { required_string("+POF to Add:"); stuff_string(appendingPOF, F_FILESPEC); required_string("+Source Path:"); stuff_string(sourcepath, F_NAME); if (optional_string("+Rename Path:")) { SCP_string name; stuff_string(name, F_NAME); renamePath = std::move(name); } if (optional_string("+Parent Submodel:")) { SCP_string name; stuff_string(name, F_NAME); targetParentSubsystem = std::move(name); } } void VirtualPOFOperationAddPath::process(polymodel* pm, model_read_deferred_tasks& /*deferredTasks*/, model_parse_depth depth, const VirtualPOFDefinition& virtualPof) const { auto appendingPMholder = load_source_pof(appendingPOF, depth, virtualPof); if (appendingPMholder == nullptr) return; const polymodel* appendingPM = appendingPMholder->pm(); int sourcePathNr = -1; for (int i = 0; i < appendingPM->n_paths; i++) { if (lcase_equal(sourcepath, appendingPM->paths[i].name)) { sourcePathNr = i; break; } } const SCP_string& targetName = renamePath ? *renamePath : sourcepath; for (int i = 0; i < pm->n_paths; i++) { if (lcase_equal(targetName, pm->paths[i].name)) { Warning(LOCATION, "Path %s already exists on POF %s for virtual POF %s. Returning original POF", targetName.c_str(), pm->filename, virtualPof.name.c_str()); return; } } if (sourcePathNr < 0) { Warning(LOCATION, "Could not find path %s on POF %s for virtual POF %s. Returning original POF", sourcepath.c_str(), appendingPOF.c_str(), virtualPof.name.c_str()); return; } int submodel_index = -1; bool clear_parent = false; if (targetParentSubsystem) { if (lcase_equal(*targetParentSubsystem, "")) clear_parent = true; else submodel_index = model_find_submodel_index(pm, targetParentSubsystem->c_str()); //Not finding this is okay, it means that the parent is a subsystem, not a submodel. Blame whoever designed this field to be double-function } if (appendingPM->paths[sourcePathNr].parent_submodel >= 0 && submodel_index < 0) { Warning(LOCATION, "Path %s must have a parent submodel specified for virtual POF %s. Returning original POF", sourcepath.c_str(), virtualPof.name.c_str()); return; } int destpath = reallocate_and_copy_array(pm->paths, pm->n_paths, 1); pm->paths[destpath] = object_copy_including_array_member(appendingPM->paths[sourcePathNr], &model_path::nverts, &model_path::verts); if (targetParentSubsystem) { if(clear_parent){ pm->paths[destpath].parent_name[0] = '\0'; pm->paths[destpath].parent_submodel = -1; } else { strcpy_s(pm->paths[destpath].parent_name, targetParentSubsystem->c_str()); pm->paths[destpath].parent_submodel = submodel_index; } } if (renamePath) { strcpy_s(pm->paths[destpath].name, renamePath->c_str()); } } VirtualPOFOperationRenameSubobjects::VirtualPOFOperationRenameSubobjects() { while (optional_string("+Replace:")) { SCP_string replace; stuff_string(replace, F_NAME); required_string("+With:"); stuff_string(replacements[replace], F_NAME); } } void VirtualPOFOperationRenameSubobjects::process(polymodel* pm, model_read_deferred_tasks& deferredTasks, model_parse_depth /*depth*/, const VirtualPOFDefinition& /*virtualPof*/) const { for (int i = 0; i < pm->n_models; i++) change_submodel_name(pm->submodel[i], replacements); decltype(deferredTasks.model_subsystems) copy_model_subsys; for (const auto& entry : deferredTasks.model_subsystems) copy_model_subsys.emplace(change_submodel_name(model_read_deferred_tasks::model_subsystem_pair(entry), replacements)); deferredTasks.model_subsystems = std::move(copy_model_subsys); decltype(deferredTasks.engine_subsystems) copy_engine_subsys; for (const auto& entry : deferredTasks.engine_subsystems) copy_engine_subsys.emplace(change_submodel_name(model_read_deferred_tasks::engine_subsystem_pair(entry), replacements)); deferredTasks.engine_subsystems = std::move(copy_engine_subsys); } VirtualPOFOperationChangeSubsystemData::VirtualPOFOperationChangeSubsystemData() { required_string("+Subsystem:"); stuff_string(subsystem, F_NAME); if (optional_string("+Set Position:")) { vec3d& offset = setPosition.emplace(); stuff_vec3d(&offset); } if (optional_string("+Set Radius:")) { float& rad = setRadius.emplace(); stuff_float(&rad); } if (optional_string("+Set Properties:")) { SCP_string& props = setProperties.emplace(); stuff_string(props, F_MULTITEXT); } else if (optional_string("+Append Properties:")) { propertyReplace = false; SCP_string& props = setProperties.emplace(); stuff_string(props, F_MULTITEXT); } } void VirtualPOFOperationChangeSubsystemData::process(polymodel* /*pm*/, model_read_deferred_tasks& deferredTasks, model_parse_depth /*depth*/, const VirtualPOFDefinition& virtualPof) const { auto subsys_it = deferredTasks.model_subsystems.find(subsystem); if (subsys_it == deferredTasks.model_subsystems.end()){ Warning(LOCATION, "Failed to find subsystem %s to change data of in virtual POF %s. Returning original POF", subsystem.c_str(), virtualPof.name.c_str()); return; } model_read_deferred_tasks::model_subsystem_parse& subsys = subsys_it->second; if (setPosition) { if (subsys.subobj_nr < 0) { subsys.pnt = *setPosition; } else Warning(LOCATION, "Cannot change position of modelled subsystem %s in virtual POF %s. Not modifying position", subsystem.c_str(), virtualPof.name.c_str()); } if (setRadius) { if (subsys.subobj_nr < 0) { subsys.rad = *setRadius; } else Warning(LOCATION, "Cannot change radius of modelled subsystem %s in virtual POF %s. Not modifying radius", subsystem.c_str(), virtualPof.name.c_str()); } if (setProperties) { if (propertyReplace) subsys.props = *setProperties; else subsys.props += '\n' + *setProperties; } } VirtualPOFOperationChangeData::VirtualPOFOperationChangeData() { required_string("+Submodel:"); stuff_string(submodel, F_NAME); if (optional_string("+Set Offset:")) { vec3d& offset = setOffset.emplace(); stuff_vec3d(&offset); } } void VirtualPOFOperationChangeData::process(polymodel* pm, model_read_deferred_tasks& deferredTasks, model_parse_depth /*depth*/, const VirtualPOFDefinition& virtualPof) const { int subobj_no = model_find_submodel_index(pm, submodel.c_str()); if (subobj_no == -1) { Warning(LOCATION, "Failed to find submodel %s to change data of in virtual POF %s. Returning original POF", submodel.c_str(), virtualPof.name.c_str()); return; } if (setOffset) { pm->submodel[subobj_no].offset = *setOffset; auto it = deferredTasks.model_subsystems.find(submodel); if (it != deferredTasks.model_subsystems.end()) { it->second.pnt = *setOffset; } } } VirtualPOFOperationHeaderData::VirtualPOFOperationHeaderData() { if (optional_string("+Set Radius:")) { radius = 0.0f; stuff_float(&(*radius)); } if (optional_string("$Set Bounding Box:")) { auto& bb = boundingbox.emplace(); required_string("+Minimum:"); stuff_vec3d(&bb.first); required_string("+Maximum:"); stuff_vec3d(&bb.second); } } void VirtualPOFOperationHeaderData::process(polymodel* pm, model_read_deferred_tasks& /*deferredTasks*/, model_parse_depth /*depth*/, const VirtualPOFDefinition& /*virtualPof*/) const { if (radius) { pm->rad = pm->core_radius = *radius; } if (boundingbox) { pm->mins = (*boundingbox).first; pm->maxs = (*boundingbox).second; model_calc_bound_box(pm->bounding_box, &pm->mins, &pm->maxs); } }