// methods and members common to any mission editor FSO may have #include "common.h" #include "ai/ai.h" #include "cmdline/cmdline.h" #include "controlconfig/controlsconfig.h" #include "globalincs/linklist.h" #include "mission/missionparse.h" #include "iff_defs/iff_defs.h" #include "io/cursor.h" #include "jumpnode/jumpnode.h" #include "localization/fhash.h" #include "mission/missionbriefcommon.h" #include "mission/missioncampaign.h" #include "model/model.h" #include "object/object.h" #include "object/waypoint.h" #include "parse/sexp.h" #include "prop/prop.h" #include "render/batching.h" #include "ship/ship.h" #include "weapon/weapon.h" #include // to keep track of data char Voice_abbrev_briefing[NAME_LENGTH]; char Voice_abbrev_campaign[NAME_LENGTH]; char Voice_abbrev_command_briefing[NAME_LENGTH]; char Voice_abbrev_debriefing[NAME_LENGTH]; char Voice_abbrev_message[NAME_LENGTH]; char Voice_abbrev_mission[NAME_LENGTH]; bool Voice_no_replace_filenames; char Voice_script_entry_format[NOTES_LENGTH]; int Voice_export_selection; // 0=everything, 1=cmd brief, 2=brief, 3=debrief, 4=messages bool Voice_group_messages; SCP_string Voice_script_default_string = "Sender: $sender\r\nPersona: $persona\r\nFile: $filename\r\nMessage: $message"; SCP_string Voice_script_instructions_string = "$name - name of the message\r\n" "$filename - name of the message file\r\n" "$message - text of the message\r\n" "$persona - persona of the sender\r\n" "$sender - name of the sender\r\n" "$note - message notes\r\n\r\n" "Note that $persona and $sender will only appear for the Message section."; float normalize_degrees(float deg) { while (deg < -180.0f) deg += 360.0f; while (deg > 180.0f) deg -= 360.0f; // check for negative zero if (deg == -0.0f) deg = 0.0f; return deg; } void time_to_mission_info_string(const std::tm* src, char* dest, size_t dest_max_len) { std::strftime(dest, dest_max_len, "%x at %X", src); } void stuff_special_arrival_anchor_name(char* buf, int iff_index, int restrict_to_players, bool retail_format) { const char* iff_name = Iff_info[iff_index].iff_name; // stupid retail hack if (retail_format && !stricmp(iff_name, "hostile") && !restrict_to_players) iff_name = "enemy"; if (restrict_to_players) sprintf(buf, "", iff_name); else sprintf(buf, "", iff_name); strlwr(buf); } void stuff_special_arrival_anchor_name(char* buf, int anchor_num, bool retail_format) { // filter out iff int iff_index = anchor_num; iff_index &= ~ANCHOR_SPECIAL_ARRIVAL; iff_index &= ~ANCHOR_SPECIAL_ARRIVAL_PLAYER; // filter players int restrict_to_players = (anchor_num & ANCHOR_SPECIAL_ARRIVAL_PLAYER); // get name stuff_special_arrival_anchor_name(buf, iff_index, restrict_to_players, retail_format); } // Ship and wing arrival and departure anchors should always be ship registry entry indexes, except for a very brief window during mission parsing. // But FRED and QtFRED dialogs use ship indexes instead. So, rather than refactor all the dialogs, this converts between the two. If an anchor // is a valid ship registry index, the equivalent ship index is returned; otherwise the special value (-1 or a flag) is returned instead. int anchor_to_target(anchor_t anchor) { auto anchor_entry = ship_registry_get(anchor); return anchor_entry ? anchor_entry->shipnum : anchor.value(); } // Ship and wing arrival and departure anchors should always be ship registry entry indexes, except for a very brief window during mission parsing. // But FRED and QtFRED dialogs use ship indexes instead. So, rather than refactor all the dialogs, this converts between the two. If a target // is a valid ship index, the equivalent ship registry index is returned; otherwise the special value (-1 or a flag) is returned instead. anchor_t target_to_anchor(int target) { if (target >= 0 && target < MAX_SHIPS) return anchor_t(ship_registry_get_index(Ships[target].ship_name)); else return anchor_t(target); } void update_custom_wing_indexes() { int i; for (i = 0; i < MAX_STARTING_WINGS; i++) Starting_wings[i] = wing_name_lookup(Starting_wing_names[i], 1); for (i = 0; i < MAX_SQUADRON_WINGS; i++) Squadron_wings[i] = wing_name_lookup(Squadron_wing_names[i], 1); for (i = 0; i < MAX_TVT_WINGS; i++) TVT_wings[i] = wing_name_lookup(TVT_wing_names[i], 1); } void generate_ship_usage_list_team(int team, SCP_map& usage) { usage.clear(); if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { Assert(team >= 0 && team < MAX_TVT_TEAMS); for (int i = 0; i < MAX_TVT_WINGS_PER_TEAM; i++) generate_ship_usage_list_wing(TVT_wings[(team * MAX_TVT_WINGS_PER_TEAM) + i], usage); } else { for (int starting_wing: Starting_wings) generate_ship_usage_list_wing(starting_wing, usage); } } void generate_ship_usage_list_wing(int wing_num, SCP_map& usage) { int i; if (wing_num < 0) { return; } i = Wings[wing_num].wave_count; while (i--) { usage[Ships[Wings[wing_num].ship_index[i]].ship_info_index]++; } } void generate_weaponry_usage_list_team(int team, SCP_map& usage) { int i; usage.clear(); if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { Assert(team >= 0 && team < MAX_TVT_TEAMS); for (i = 0; i < MAX_TVT_WINGS_PER_TEAM; i++) { generate_weaponry_usage_list_wing(TVT_wings[(team * MAX_TVT_WINGS_PER_TEAM) + i], usage); } } else { for (i = 0; i < MAX_STARTING_WINGS; i++) { generate_weaponry_usage_list_wing(Starting_wings[i], usage); } } } void generate_weaponry_usage_list_wing(int wing_num, SCP_map& usage) { int i, j; ship_weapon* swp; if (wing_num < 0) { return; } i = Wings[wing_num].wave_count; while (i--) { swp = &Ships[Wings[wing_num].ship_index[i]].weapons; j = swp->num_primary_banks; while (j--) { if (Weapon_info.in_bounds(swp->primary_bank_weapons[j])) { usage[swp->primary_bank_weapons[j]]++; } } j = swp->num_secondary_banks; while (j--) { if (Weapon_info.in_bounds(swp->secondary_bank_weapons[j])) { usage[swp->secondary_bank_weapons[j]] += sz2i(floor((swp->secondary_bank_ammo[j] * swp->secondary_bank_capacity[j] / 100.0f / Weapon_info[swp->secondary_bank_weapons[j]].cargo_size) + 0.5f)); } } } } void ensure_valid_player_start_shipnum() { // nothing to do if the current player start is still valid if (Player_start_shipnum >= 0 && Player_start_shipnum < MAX_SHIPS && Ships[Player_start_shipnum].objnum >= 0 && Objects[Ships[Player_start_shipnum].objnum].type == OBJ_START) { return; } // otherwise repoint to the first remaining player start, or -1 if there are none Player_start_shipnum = -1; for (auto *objp : list_range(&obj_used_list)) { if (objp->type == OBJ_START) { Player_start_shipnum = objp->instance; break; } } } bool set_single_player_start(int objnum) { if (objnum < 0 || objnum >= MAX_OBJECTS || (Objects[objnum].type != OBJ_SHIP && Objects[objnum].type != OBJ_START)) { Assertion(false, "set_single_player_start() called for object %d, which is not a ship", objnum); return false; } bool changed = false; for (auto *objp : list_range(&obj_used_list)) { if (objp->type != OBJ_SHIP && objp->type != OBJ_START) continue; if (OBJ_INDEX(objp) == objnum) { // set as player ship if (objp->type != OBJ_START) { objp->type = OBJ_START; Player_starts++; changed = true; } objp->flags.set(Object::Object_Flags::Player_ship); } else { // set as regular ship if (objp->type == OBJ_START) { objp->type = OBJ_SHIP; Player_starts--; changed = true; } objp->flags.remove(Object::Object_Flags::Player_ship); } } ensure_valid_player_start_shipnum(); return changed; } SCP_string check_name_conflict(const char *entity_type, const char *name, int exclude_ship, int exclude_wing, int exclude_waypoint_list, int exclude_jump_node) { SCP_string msg; // Name must not be empty if (name[0] == '\0') { msg += "This "; msg += entity_type; msg += " name cannot be empty"; return msg; } // Check wings for (int i = 0; i < MAX_WINGS; i++) { if (Wings[i].wave_count && i != exclude_wing && !stricmp(Wings[i].name, name)) { msg += "This "; msg += entity_type; msg += " name is already being used by "; msg += (exclude_wing >= 0) ? "another wing" : "a wing"; return msg; } } // Check ships for (auto ptr: list_range(&obj_used_list)) { if ((ptr->type == OBJ_SHIP || ptr->type == OBJ_START) && ptr->instance != exclude_ship) { if (!stricmp(Ships[ptr->instance].ship_name, name)) { msg += "This "; msg += entity_type; msg += " name is already being used by "; msg += (exclude_ship >= 0) ? "another ship" : "a ship"; return msg; } } } // We don't need to check teams. "Unknown" is a valid name and also an IFF. // Check target priority groups for (const auto &ai_tp: Ai_tp_list) { if (!stricmp(ai_tp.name, name)) { msg += "This "; msg += entity_type; msg += " name is already being used by a target priority group"; return msg; } } // Check waypoint lists int wl_size = sz2i(Waypoint_lists.size()); for (int i = 0; i < wl_size; i++) { if (i != exclude_waypoint_list && !stricmp(Waypoint_lists[i].get_name(), name)) { msg += "This "; msg += entity_type; msg += " name is already being used by "; msg += (exclude_waypoint_list >= 0) ? "another waypoint path" : "a waypoint path"; return msg; } } // Check jump nodes int jn_size = sz2i(Jump_nodes.size()); for (int i = 0; i < jn_size; i++) { if (i != exclude_jump_node && !stricmp(Jump_nodes[i].GetName(), name)) { msg += "This "; msg += entity_type; msg += " name is already being used by "; msg += (exclude_jump_node >= 0) ? "another jump node" : "a jump node"; return msg; } } // Name must not start with '<' (used for invalidated SEXP references) if (name[0] == '<') { msg += "This "; msg += entity_type; msg += " name is not allowed to begin with <"; return msg; } return ""; // no error } void editor_free_engine_data() { // models, model instances, and collision trees; done before ship_close() because model_unload() walks Ship_info model_free_all(); // weapon and ship table data, and ship subsystems weapon_close(); ship_close(); // the editors hold strdup'd media names in every message, including the builtin ones for (auto &msg : Messages) message_free_media_names(msg); mission_parse_close(); mission_brief_common_close(); fhash_flush(); control_config_common_close(); sexp_shutdown(); batching_shutdown(); // the cursors hold bitmaps, so this must happen before the graphics system shuts down io::mouse::CursorManager::shutdown(); if (Cmdline_mod != nullptr) { delete[] Cmdline_mod; Cmdline_mod = nullptr; } } int load_and_find_campaign_mission(const char *mission_filename) { if (!mission_filename || !mission_filename[0]) return -1; int idx = mission_campaign_find_mission(mission_filename); if (idx < 0) return -1; if (Campaign.missions[idx].flags & CMISSION_FLAG_FRED_LOAD_PENDING) { read_mission_goal_list(idx); Campaign.missions[idx].flags &= ~CMISSION_FLAG_FRED_LOAD_PENDING; } return idx; } void reassign_ship_slot(int from, int to, const FredShipSlotConfig& cfg, bool resort_obj_list) { Assertion(Fred_running, "reassign_ship_slot is FRED-only: it does not fix up game-context references like Player_ship or runtime Ai_info shipnums"); Assertion(from != to, "reassign_ship_slot: from == to (%d)", from); Assertion(from >= 0 && from < MAX_SHIPS, "reassign_ship_slot: 'from' slot %d out of range", from); Assertion(to >= 0 && to < MAX_SHIPS, "reassign_ship_slot: 'to' slot %d out of range", to); Assertion(Ships[from].objnum >= 0, "reassign_ship_slot: source slot %d is empty", from); Assertion(Ships[to].objnum < 0, "reassign_ship_slot: destination slot %d is occupied", to); // Move the ship struct itself. Ship's move operations handle the members // that need care (the subsys_list sentinel re-links its bookend nodes, and // the owning pointers are unique_ptrs); this function's job is the external // back-references. Per the engine's convention, a slot with objnum < 0 is // considered empty. Ships[to] = std::move(Ships[from]); Ships[from].objnum = -1; // Move FRED-side parallel arrays if the caller supplied them. if (cfg.fred_alt_names != nullptr) { strcpy_s(cfg.fred_alt_names[to], cfg.fred_alt_names[from]); cfg.fred_alt_names[from][0] = '\0'; } if (cfg.fred_callsigns != nullptr) { strcpy_s(cfg.fred_callsigns[to], cfg.fred_callsigns[from]); cfg.fred_callsigns[from][0] = '\0'; } // Object back-reference. Objects[Ships[to].objnum].instance = to; // Keep obj_used_list iteration order in sync with Ships[] slot order. The // re-sort is a total rebuild rather than an incremental fix, so a caller // making a batch of reassignments may defer it to the final call. if (resort_obj_list) resort_ships_in_obj_used_list(); // AI back-reference (the one invariant codified by internal_integrity_check). if (Ships[to].ai_index >= 0) Ai_info[Ships[to].ai_index].shipnum = to; // Wing membership: scan every wing and re-point any reference to the old slot. // (wing.special_ship is wing-relative, NOT a Ships[] index, so it is intentionally // not touched here.) for (auto &w: Wings) { if (w.wave_count == 0) continue; for (int k = 0; k < w.wave_count; ++k) { if (w.ship_index[k] == from) w.ship_index[k] = to; } } // Single-player start. if (Player_start_shipnum == from) Player_start_shipnum = to; // Ship_registry caches the shipnum on its entries (lookup is by name, but the // cached integer would otherwise go stale). int reg = ship_registry_get_index(Ships[to].ship_name); if (reg >= 0) Ship_registry[reg].shipnum = to; // FRED's current-ship pointer, if the caller is tracking one. if (cfg.cur_ship != nullptr && *cfg.cur_ship == from) *cfg.cur_ship = to; } static bool ship_slot_is_empty(int i) { return Ships[i].objnum < 0; } static bool wing_slot_is_empty(int i) { return Wings[i].wave_count == 0; } static int find_free_slot(int max_slots, bool (*slot_is_empty)(int)) { for (int i = 0; i < max_slots; ++i) { if (slot_is_empty(i)) return i; } return -1; } template static void swap_slots(int a, int b, const TConfig& cfg, int max_slots, bool (*slot_is_empty)(int), void (*reassign)(int, int, const TConfig&, bool), const char* caller) { if (a == b) return; Assertion(a >= 0 && a < max_slots, "%s: slot 'a' %d out of range", caller, a); Assertion(b >= 0 && b < max_slots, "%s: slot 'b' %d out of range", caller, b); Assertion(!slot_is_empty(a) && !slot_is_empty(b), "%s: both slots must be valid (a=%d, b=%d)", caller, a, b); // Find a free temporary slot. int tmp = find_free_slot(max_slots, slot_is_empty); if (tmp < 0) { ReleaseWarning(LOCATION, "%s: no free slot available for the temporary leg", caller); return; } // Three-leg swap; each call's preconditions hold by construction. The // total-rebuild fixups are deferred to the final leg. reassign(a, tmp, cfg, false); reassign(b, a, cfg, false); reassign(tmp, b, cfg, true); } template static void rotate_slots(const SCP_vector& slots, int from_pos, int to_pos, const TConfig& cfg, int max_slots, bool (*slot_is_empty)(int), void (*reassign)(int, int, const TConfig&, bool), const char* caller) { if (from_pos == to_pos) return; int count = static_cast(slots.size()); Assertion(from_pos >= 0 && from_pos < count, "%s: 'from' position %d out of range", caller, from_pos); Assertion(to_pos >= 0 && to_pos < count, "%s: 'to' position %d out of range", caller, to_pos); // Find a free temporary slot. int tmp = find_free_slot(max_slots, slot_is_empty); if (tmp < 0) { ReleaseWarning(LOCATION, "%s: no free slot available for the temporary leg", caller); return; } // Park the moving item in the free slot, shift everything between the two // positions over by one, then drop the item into the slot vacated at the // far end. Preserves the relative order of everything else, in K+2 // reassignments for a move of K positions (vs 3K for a bubble of swaps). // The total-rebuild fixups are deferred to the final leg. int step = (to_pos > from_pos) ? 1 : -1; reassign(slots[from_pos], tmp, cfg, false); for (int j = from_pos; j != to_pos; j += step) reassign(slots[j + step], slots[j], cfg, false); reassign(tmp, slots[to_pos], cfg, true); } void swap_ship_slots(int a, int b, const FredShipSlotConfig& cfg) { swap_slots(a, b, cfg, MAX_SHIPS, ship_slot_is_empty, reassign_ship_slot, "swap_ship_slots"); } void rotate_ship_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredShipSlotConfig& cfg) { rotate_slots(slots, from_pos, to_pos, cfg, MAX_SHIPS, ship_slot_is_empty, reassign_ship_slot, "rotate_ship_slots"); } void reassign_wing_slot(int from, int to, const FredWingSlotConfig& cfg, bool update_wing_indexes) { Assertion(Fred_running, "reassign_wing_slot is FRED-only"); Assertion(from != to, "reassign_wing_slot: from == to (%d)", from); Assertion(from >= 0 && from < MAX_WINGS, "reassign_wing_slot: 'from' slot %d out of range", from); Assertion(to >= 0 && to < MAX_WINGS, "reassign_wing_slot: 'to' slot %d out of range", to); Assertion(Wings[from].wave_count > 0, "reassign_wing_slot: source slot %d is empty", from); Assertion(Wings[to].wave_count == 0, "reassign_wing_slot: destination slot %d is occupied", to); // Move the wing struct itself; wave_count == 0 is the sentinel for an empty wing. Wings[to] = std::move(Wings[from]); Wings[from].wave_count = 0; // Move FRED-side parallel array if the caller supplied it. if (cfg.wing_objects != nullptr) { for (int k = 0; k < MAX_SHIPS_PER_WING; ++k) { cfg.wing_objects[to][k] = cfg.wing_objects[from][k]; cfg.wing_objects[from][k] = -1; } } // Per-ship parent-wing back-reference. for (auto &s: Ships) { if (s.objnum < 0) continue; if (s.wingnum == from) s.wingnum = to; } // FRED's current-wing pointer, if the caller is tracking one. if (cfg.cur_wing != nullptr && *cfg.cur_wing == from) *cfg.cur_wing = to; // Rebuild Starting/Squadron/TVT_wings caches from the parallel name arrays. // The rebuild is total rather than incremental, so a caller making a batch // of reassignments may defer it to the final call. if (update_wing_indexes) update_custom_wing_indexes(); } void swap_wing_slots(int a, int b, const FredWingSlotConfig& cfg) { swap_slots(a, b, cfg, MAX_WINGS, wing_slot_is_empty, reassign_wing_slot, "swap_wing_slots"); } void rotate_wing_slots(const SCP_vector& slots, int from_pos, int to_pos, const FredWingSlotConfig& cfg) { rotate_slots(slots, from_pos, to_pos, cfg, MAX_WINGS, wing_slot_is_empty, reassign_wing_slot, "rotate_wing_slots"); } // Bulk-re-sort one type's subset of obj_used_list while keeping non-matching // entries in their original relative positions. Each callsite supplies a // type matcher and a key function; the i-th matching slot (in original list // order) receives the i-th smallest matching node by key. static void resort_obj_used_list_subset( bool (*matches_type)(int), int (*key)(const object*)) { SCP_vector all; SCP_vector matched; for (auto o : list_range(&obj_used_list)) { all.push_back(o); if (matches_type(o->type)) matched.push_back(o); } std::sort(matched.begin(), matched.end(), [&](const object* a, const object* b) { return key(a) < key(b); }); list_init(&obj_used_list); auto it = matched.begin(); for (auto o : all) { if (matches_type(o->type)) { list_append(&obj_used_list, *it); ++it; } else { list_append(&obj_used_list, o); } } } void resort_ships_in_obj_used_list() { resort_obj_used_list_subset( [](int t) { return t == OBJ_SHIP || t == OBJ_START; }, [](const object* o) { return o->instance; }); } void resort_props_in_obj_used_list() { resort_obj_used_list_subset( [](int t) { return t == OBJ_PROP; }, [](const object* o) { return o->instance; }); } void rotate_prop_slots(const SCP_vector& slots, int from_pos, int to_pos) { Assertion(Fred_running, "rotate_prop_slots is FRED-only: it re-points object instances only, not any game-context state"); if (from_pos == to_pos) return; int count = static_cast(slots.size()); Assertion(from_pos >= 0 && from_pos < count, "rotate_prop_slots: 'from' position %d out of range", from_pos); Assertion(to_pos >= 0 && to_pos < count, "rotate_prop_slots: 'to' position %d out of range", to_pos); // Permute the prop occupants among the slot indices, leaving any empty // holes where they are. Park the mover, shift the rest over by one, then drop // the mover into the slot vacated at the far end. std::optional moving = std::move(Props[slots[from_pos]]); int step = (to_pos > from_pos) ? 1 : -1; for (int j = from_pos; j != to_pos; j += step) Props[slots[j]] = std::move(Props[slots[j + step]]); Props[slots[to_pos]] = std::move(moving); // Re-point each moved prop's object instance to its new Props[] index. Only // the occupants between the two positions changed slots. int lo = std::min(from_pos, to_pos); int hi = std::max(from_pos, to_pos); for (int j = lo; j <= hi; ++j) { if (Props[slots[j]].has_value()) Objects[Props[slots[j]]->objnum].instance = slots[j]; } // Keep obj_used_list prop order in sync with Props[] index order, so // the Scene Browser reflects the new order too. resort_props_in_obj_used_list(); } void rotate_waypoint_lists(int from_pos, int to_pos) { Assertion(Fred_running, "rotate_waypoint_lists is FRED-only: it re-points object instances only, not any game-context state"); if (from_pos == to_pos) return; int count = static_cast(Waypoint_lists.size()); Assertion(from_pos >= 0 && from_pos < count, "rotate_waypoint_lists: 'from' position %d out of range", from_pos); Assertion(to_pos >= 0 && to_pos < count, "rotate_waypoint_lists: 'to' position %d out of range", to_pos); // Reorder the lists themselves, preserving the relative order of the rest. if (from_pos < to_pos) std::rotate(Waypoint_lists.begin() + from_pos, Waypoint_lists.begin() + from_pos + 1, Waypoint_lists.begin() + to_pos + 1); else std::rotate(Waypoint_lists.begin() + to_pos, Waypoint_lists.begin() + from_pos, Waypoint_lists.begin() + from_pos + 1); // A waypoint object encodes its list index, point index in its instance, so // every waypoint in the shifted lists now sits at a new list index. Rebuild // all instances from the lists' current positions. int li = 0; for (auto& list : Waypoint_lists) { int wi = 0; for (auto& wpt : list.get_waypoints()) { int objnum = wpt.get_objnum(); if (objnum >= 0) Objects[objnum].instance = calc_waypoint_instance(li, wi); ++wi; } ++li; } } void rotate_jump_nodes(const SCP_vector& slots, int from_pos, int to_pos) { Assertion(Fred_running, "rotate_jump_nodes is FRED-only: it reorders the editor's jump-node list only"); if (from_pos == to_pos) return; int count = static_cast(slots.size()); Assertion(from_pos >= 0 && from_pos < count, "rotate_jump_nodes: 'from' position %d out of range", from_pos); Assertion(to_pos >= 0 && to_pos < count, "rotate_jump_nodes: 'to' position %d out of range", to_pos); // Permute the node occupants among the fixed slot indices, leaving any // detached entries in place. Park the mover, shift the rest over by one, then // drop the mover into the slot vacated at the far end. CJumpNode moving = std::move(Jump_nodes[slots[from_pos]]); int step = (to_pos > from_pos) ? 1 : -1; for (int j = from_pos; j != to_pos; j += step) Jump_nodes[slots[j]] = std::move(Jump_nodes[slots[j + step]]); Jump_nodes[slots[to_pos]] = std::move(moving); } bool get_object_bounds(bool marked_only, vec3d* center, float* radius) { vec3d mins = ZERO_VECTOR, maxs = ZERO_VECTOR; bool found = false; for (auto* objp : list_range(&obj_used_list)) { if (marked_only && !objp->flags[Object::Object_Flags::Marked]) { continue; } if (!found) { mins = maxs = objp->pos; found = true; continue; } mins.xyz.x = std::min(mins.xyz.x, objp->pos.xyz.x); mins.xyz.y = std::min(mins.xyz.y, objp->pos.xyz.y); mins.xyz.z = std::min(mins.xyz.z, objp->pos.xyz.z); maxs.xyz.x = std::max(maxs.xyz.x, objp->pos.xyz.x); maxs.xyz.y = std::max(maxs.xyz.y, objp->pos.xyz.y); maxs.xyz.z = std::max(maxs.xyz.z, objp->pos.xyz.z); } if (!found) { return false; } vm_vec_avg(center, &mins, &maxs); *radius = 0.0f; for (auto* objp : list_range(&obj_used_list)) { if (marked_only && !objp->flags[Object::Object_Flags::Marked]) { continue; } *radius = std::max(*radius, vm_vec_dist(center, &objp->pos)); } return true; }